Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Return Complex Data From Main

0.00/5 (No votes)
22 Feb 2015 1  
How to return strings or other complex data from a process to its caller without interprocess communication

Introduction

This article is going to show a way to return other values than integers from an application.

C#
static int Main(string[] args)
{
    //...
    return 0;  // Other things than int?
}

On the Windows operating system, an application’s return value is stored at an environment variable named %ERRORLEVEL%. This value can also be obtained in C# by getting the Exitcode property of a process.

Of course, the actual value of the main method must be an integer, but returning a value just means that the caller gets the result somewhere into his own memory. The easiest way to implement this would be to write to the standard output of the process and read the data from there, but what if we need the standard output for something else?

There is a solution: We could just marshal any data into the callers address space and return the address of the data.

If you only want to test it, feel free to download the demo project. If you want to implement it, the full source is at the end of this article.

This demo app takes two numbers as arguments and returns a string and some random numbers to the calling process:

1) Getting the Calling Process (Parent)

In order to change data in the calling process, we have to find out which process this might be. This can be achieved by CreateToolhelp32Snapshot. Unfortunately, it is not really as straightforward as one would expect:

C#
[DllImport("kernel32.dll", SetLastError = true)]
static extern IntPtr CreateToolhelp32Snapshot(uint dwFlags, uint th32ProcessID);

[DllImport("kernel32.dll")]
static extern bool Process32First(IntPtr hSnapshot, ref PROCESSENTRY32 lppe);

[DllImport("kernel32.dll")]
static extern bool Process32Next(IntPtr hSnapshot, ref PROCESSENTRY32 lppe);

private static Process GetParentProcess(int Id)
{
    int iParentPid = 0;
    int iCurrentPid = Process.GetCurrentProcess().Id;

    IntPtr oHnd = CreateToolhelp32Snapshot(2, 0);
    if (oHnd == IntPtr.Zero) return null;

    PROCESSENTRY32 oProcInfo = new PROCESSENTRY32();

    oProcInfo.dwSize = (uint)Marshal.SizeOf(typeof(PROCESSENTRY32));

    if (Process32First(oHnd, ref oProcInfo) == false) { return null; }

    do
    {
        if (iCurrentPid == oProcInfo.th32ProcessID)
        {
            iParentPid = (int)oProcInfo.th32ParentProcessID;
        }
    }
    while (iParentPid == 0 && Process32Next(oHnd, ref oProcInfo));

    if (iParentPid > 0) return Process.GetProcessById(iParentPid);
    else return null;
}

With this method, we can get the Parent of any Process.

2) Writing the Data

2.1 Committing Memory

The basic ideas of writing to another process are already covered by this article:

C#
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr OpenProcess(int dwDesiredAccess, bool bInheritHandle, int dwProcessId);

//dwDesiredAccess:
//VirtualMemoryOperation = 0x00000008,
//VirtualMemoryRead = 0x00000010,
//VirtualMemoryWrite = 0x00000020,

Now it is time to write any data we want into our parent process. How can we accomplish this? First, we need a place to store it. We can't just write anywhere into the parent process because we might overwrite something. Luckily, we can just ask the target process to allocate some new memory for us:

C#
[DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
        static extern IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress,
        uint dwSize, AllocationType flAllocationType, MemoryProtection flProtect);

This method is very similar to Malloc, but it can be done at any non protected process. Memory protection should be ReadWrite = 0x04 in this case.

C#
public static IntPtr Alloc(this Process process, int Length)
{
    IntPtr processPtr = OpenProcess(ProcessAccessFlags.All, false, process.Id); 
    IntPtr Location = VirtualAllocEx(processPtr, IntPtr.Zero, (uint)Length, 
                      AllocationType.Commit, MemoryProtection.ReadWrite);

    return Location;
}

2.2 Writing to Memory

If we want to write any object to memory, we first have to convert the object to a byte array. This can be done by using binaryformatter or any other serializer. For the sake of compatibility, we can use the Marshal class:

C#
static byte[] MarshalObject(Object obj)
        {
            byte[] ValueAsBytes;
            int Size = Marshal.SizeOf(obj);
            IntPtr StartLocation = Marshal.AllocHGlobal(Size);

            Marshal.StructureToPtr(obj, StartLocation, true);
            ValueAsBytes = new byte[Size];
            Marshal.Copy(StartLocation, ValueAsBytes, 0, Size);
            Marshal.FreeHGlobal(StartLocation);
            return ValueAsBytes;
        }

Now, we have to get the byte[] to the parent process. For this, we have to commit memory with the method described in 2.1. We then use the returned IntPtr as the location for our data:

C#
[DllImport("kernel32.dll", SetLastError = true)]
        static extern bool WriteProcessMemory(
        IntPtr hProcess,
        IntPtr lpBaseAddress,
        byte[] lpBuffer,
        int nSize,
        out IntPtr lpNumberOfBytesWritten);

Just writing the data into the target process will not be enough. If we want to read it from memory, we will need the length of the data too. We can solve this by marshalling another interface to the target IntPtr.

The first 4 bytes will be the length of the data in bytes and the second 4 bytes will be the final location. The last number will tell us if the object was marshalled or serialized.

C#
[StructLayout(LayoutKind.Sequential)]
class DataInformation
{
    public int length = 0;
    public int location = 0;
    public int Marshal = 0;
}

For the complete code, see the source at the end.

3) Translate It Back

The following code can be implemented in any programming language which can invoke kernel32.dll or can read its own process memory directly. If you read memory at the address of the return code, the first 4 bytes will be the length of the data in bytes and the second 4 bytes will be the final location. If the last 4 bytes are 0, this indicates that the data was not marked as [StructLayout(LayoutKind.Sequential)].

The interface or class used must be known in advance. If that is not known, either you can always return a Json or XML string to the calling process.

This is needed because we need the location and the length of data to know exactly how many bytes we want.

C#
public static object TranslateExitCode(this Process process, int ExitCode,Type Interface)
{
    var info = (DataInformation)Marshal.PtrToStructure(new IntPtr(ExitCode), typeof(DataInformation));

    if (info.Marshal == 1)
    {
        return Marshal.PtrToStructure(new IntPtr(info.location), Interface);
    }
    else
    {
        byte[] buffer = new byte[info.length];
        Marshal.Copy(new IntPtr(info.location),buffer,0,info.length);
        MemoryStream Stream = new MemoryStream(buffer);
        return new BinaryFormatter().Deserialize(Stream);
    }
}

Using the Code

This DLL exports some extension methods for the Process class. Just include as reference and the Process class should support:

  • IntPtr Alloc(int Length)
  • void OverWriteMemory(IntPtr Destination, byte[] Data)
  • WriteMemory(byte[] data)
  • byte[] ReadMemory(Intptr Location, int Length)
  • Exit(object Value)
  • TranslateExitCode<ValueType>(int ExitCode)
  • ParentProcess()

With these extension methods, it is really easy to read and write process memory.
Any object returned by Process.Exit must be marked as [Serializable] or [StructLayout(LayoutKind.Sequential)].

Process B

C#
interface MyInterface
{
    string Description { get; set; }
    double Value { get; set; }
    public List<int> Values { get; set; }
}

[Serializable]
class ComplexInterface : MyInterface
{
    public string Description { get; set; }

    public double Value { get; set; }

    public List<int> Values { get; set; }

    public ComplexInterface()
    {
        Values = new List<int>();
    }
}
C#
static int Main(string[] args)
{
    //Do something with args
    Process.GetCurrentProcess().Exit("Any string or class here");  //return string    
    //Process.GetCurrentProcess().Exit(new ComplexInterface());    //return data
}

Process A

C#
interface IComplex
{
    string Description { get; set; }
    double Value { get; set; }
    List<int> Values { get; set; }
}

static void Main(string[] args)
{
    var start=new ProcessStartInfo("Process B.exe");
    start.Verb = "runas";   //required for PROCESS_VM_WRITE

    Process TestCalled = Process.Start(start);
    TestCalled.WaitForExit();

    int exit = TestCalled.ExitCode;
    var MyString = TestCalled.TranslateExitCode<string>(TestCalled.ExitCode);    //get string
    //var MyData = TestCalled.TranslateExitCode<IComplex>(TestCalled.ExitCode);    //get data
}

It really is very simple. The called process (B) returns and marshals the location of any data to process (A) which in turn only has to read the desired address.

Complete Source

You can use the following text directly instead of downloading the demo project source:

C#
using System.ComponentModel;
using System.IO;
using System.Runtime.InteropServices;
using System.Runtime.Serialization.Formatters.Binary;

namespace System.Diagnostics
{
    /// <summary>
    /// Provides static extension methods to marshal data in and out of the process class
    /// </summary>
    public static class ProcessReturnHelper
    {
        /// <summary>
        /// Writes the specified object into the parent process and terminates 
        /// with the Marshalled location of a ProcessReturnHelper.DataInformation class
        /// Value must be marked with [StructLayout(LayoutKind.Sequential)] or [Serializable]
        /// </summary>
        /// <param name="process"></param>
        /// <param name="Value">Value has to implement StructLayout(LayoutKind.Sequential) 
        /// or has to implement [Serializable]</param>
        public static void Exit(this Process process, object Value, bool ForceMarshal = false)
        {
            if (process.Id != Process.GetCurrentProcess().Id)
            {
                throw new ArgumentException("You can only exit your own process");
            }

            Type ValueType = Value.GetType();
            byte[] ValueAsBytes;
            bool WasMarshalled = false;

            if (ValueType.IsLayoutSequential)
            {
                ValueAsBytes = MarshalObject(Value);
                WasMarshalled = true;
            }
            else if (ValueType.IsSerializable)
            {
                if (ForceMarshal == true) { throw new ArgumentException
                   ("Value is not [StructLayout(LayoutKind.Sequential)] 
                   but ForceMarshal was specified as true"); }
                ValueAsBytes = SerializeObject(Value);
            }
            else
            {
                throw new ArgumentException("Value cannot be marshalled nor serialized!");
            }

            Process ParentProcess = GetParentProcess(Process.GetCurrentProcess().Id);
            if (ParentProcess == null) 
                { throw new InvalidOperationException("Parent process not found"); }

            int Location = ParentProcess.WriteMemory(ValueAsBytes).ToInt32();

            DataInformation Header = new DataInformation(WasMarshalled, ValueAsBytes, Location);
            int LocationOfInformation = ParentProcess.WriteMemory(MarshalObject(Header)).ToInt32();

            Environment.Exit(LocationOfInformation);
        }

        /// <summary>
        /// Returns an object returned by a process by Process.Exit(Value), extension method
        /// </summary>
        /// <param name="process"></param>
        /// <param name="ExitCode">The Exit Code of the Process</param>
        /// <param name="Interface"></param>
        /// <returns></returns>
        public static object TranslateExitCode(this Process process, int ExitCode,Type Interface)
        {
            var info = (DataInformation)Marshal.PtrToStructure(new IntPtr(ExitCode), 
                        typeof(DataInformation));

            if (info.Marshal == 1)
            {
                return Marshal.PtrToStructure(new IntPtr(info.location), Interface);
            }
            else
            {
                byte[] buffer = new byte[info.length];
                Marshal.Copy(new IntPtr(info.location),buffer,0,info.length);
                MemoryStream Stream = new MemoryStream(buffer);
                return new BinaryFormatter().Deserialize(Stream);
            }
        }

        /// <summary>
        /// Returns an object returned by a process by Process.Exit(Value), extension method
        /// </summary>
        /// <param name="process"></param>
        /// <param name="ExitCode">The Exit Code of the Process</param>
        public static T TranslateExitCode<T>(this Process process, int ExitCode) where T : class
        {
            return (T)TranslateExitCode(process, ExitCode, typeof(T));
        }

        [StructLayout(LayoutKind.Sequential)]
        class DataInformation
        {
            public int length = 0;
            public int location = 0;
            public int Marshal = 0;

            public DataInformation(bool Marshal,byte[] Data,int Location)
            {
                length = Data.Length;
                this.location = Location;
                if (Marshal==true)
                {
                    this.Marshal = 1;
                }
            }
            public DataInformation() { }
        }

        static byte[] MarshalObject(Object obj)
        {
            byte[] ValueAsBytes;
            int Size = Marshal.SizeOf(obj);
            IntPtr StartLocation = Marshal.AllocHGlobal(Size);

            Marshal.StructureToPtr(obj, StartLocation, true);
            ValueAsBytes = new byte[Size];
            Marshal.Copy(StartLocation, ValueAsBytes, 0, Size);
            Marshal.FreeHGlobal(StartLocation);
            return ValueAsBytes;
        }

        static byte[] SerializeObject(Object obj)
        {
            using (MemoryStream Stream = new MemoryStream())
            {
                new BinaryFormatter().Serialize(Stream, obj);
                return Stream.ToArray();
            }
        }

        /// <summary>
        /// Reads memory of the given process the given location
        /// </summary>
        /// <param name="Process"></param>
        /// <param name="Location">The Location of the data</param>
        /// <param name="Length">Number of bytes to read</param>
        /// <returns></returns>
        public static byte[] ReadMemory(this Process Process, IntPtr Location, int Length)
        {
            IntPtr processHandle = OpenProcess(Process, ProcessAccessFlags.VirtualMemoryRead); 
                                   CheckForErrors();
            byte[] Data = new byte[Length];

            IntPtr BytesRead = IntPtr.Zero;
            ReadProcessMemory(processHandle, Location, Data, Length, out BytesRead); CheckForErrors();

            if (BytesRead.ToInt32() != Data.Length)
            {
                throw new InvalidOperationException
                ("Not all bytes were written. Unnokn error"); //Checkforerrors should have thrown.
            }

            return Data;
        }

        /// <summary>
        /// Allocates memory, writes a bytearray to this location and returns the location
        /// </summary>
        /// <param name="process"></param>
        /// <param name="Data">Data to be written to a free location of the process</param>
        /// <returns></returns>
        public static IntPtr WriteMemory(this Process process, byte[] Data)
        {
            IntPtr Destination = process.Alloc(Data.Length);
            process.OverWriteMemory(Destination, Data);
            return Destination;
        }

        /// <summary>
        /// Forces to write memory to this location. If this page is unused or restricted, 
        /// it will throw an Win32Exception: Access Denied
        /// </summary>
        /// <param name="process"></param>
        /// <param name="Destination">Location of the data to be written to</param>
        /// <param name="Data">Data to be written to the location</param>
        public static void OverWriteMemory(this Process process, IntPtr Destination, byte[] Data)
        {
            IntPtr processHandle = OpenProcess(process, ProcessAccessFlags.VirtualMemoryWrite | 
                                   ProcessAccessFlags.VirtualMemoryOperation); CheckForErrors();

            IntPtr BytesWritten = IntPtr.Zero;
            WriteProcessMemory(processHandle, Destination, Data, Data.Length, out BytesWritten); 
                               CheckForErrors();
            if (BytesWritten.ToInt32()!=Data.Length)
            {
                throw new InvalidOperationException
                  ("Not all bytes were written. Unknown error"); //Checkforerrors should have thrown.
            }
        }

        /// <summary>
        /// Finds a free memory range and returns the Position
        /// </summary>
        /// <param name="process"></param>
        /// <param name="Length">Size of allocation</param>
        /// <returns></returns>
        public static IntPtr Alloc(this Process process, int Length)
        {
            IntPtr processPtr = OpenProcess(ProcessAccessFlags.All, false, process.Id); CheckForErrors();
            IntPtr Location = VirtualAllocEx(processPtr, IntPtr.Zero, 
               (uint)Length, AllocationType.Commit, MemoryProtection.ReadWrite); CheckForErrors();

            return Location;
        }

        public static Process ParentProcess(this Process process)
        {
            return GetParentProcess(process.Id);
        }

        private static void CheckForErrors()
        {
            int Error = Marshal.GetLastWin32Error();
            if (Error != 0) { throw new Win32Exception(Error); }
        }

        private static Process GetParentProcess(int Id)
        {
            int iParentPid = 0;
            int iCurrentPid = Process.GetCurrentProcess().Id;

            IntPtr oHnd = CreateToolhelp32Snapshot(2, 0);

            if (oHnd == IntPtr.Zero) return null;

            PROCESSENTRY32 oProcInfo = new PROCESSENTRY32();

            oProcInfo.dwSize = (uint)Marshal.SizeOf(typeof(PROCESSENTRY32));

            if (Process32First(oHnd, ref oProcInfo) == false) { return null; }

            do
            {
                if (iCurrentPid == oProcInfo.th32ProcessID)
                {
                    iParentPid = (int)oProcInfo.th32ParentProcessID;
                }
            }
            while (iParentPid == 0 && Process32Next(oHnd, ref oProcInfo));

            if (iParentPid > 0) return Process.GetProcessById(iParentPid);
            else return null;
        }

        #region Imports

        [StructLayout(LayoutKind.Sequential)]
        struct PROCESSENTRY32
        {
            public uint dwSize;
            public uint cntUsage;
            public uint th32ProcessID;
            public IntPtr th32DefaultHeapID;
            public uint th32ModuleID;
            public uint cntThreads;
            public uint th32ParentProcessID;
            public int pcPriClassBase;
            public uint dwFlags;
            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
            public string szExeFile;
        };

        [Flags]
        public enum ProcessAccessFlags : uint
            {
                All = 0x001F0FFF,
                Terminate = 0x00000001,
                CreateThread = 0x00000002,
                VirtualMemoryOperation = 0x00000008,
                VirtualMemoryRead = 0x00000010,
                VirtualMemoryWrite = 0x00000020,
                DuplicateHandle = 0x00000040,
                CreateProcess = 0x000000080,
                SetQuota = 0x00000100,
                SetInformation = 0x00000200,
                QueryInformation = 0x00000400,
                QueryLimitedInformation = 0x00001000,
                Synchronize = 0x00100000
            }
        [Flags]
        public enum AllocationType
        {
            Commit = 0x1000,
            Reserve = 0x2000,
            Decommit = 0x4000,
            Release = 0x8000,
            Reset = 0x80000,
            Physical = 0x400000,
            TopDown = 0x100000,
            WriteWatch = 0x200000,
            LargePages = 0x20000000
        }

        [Flags]
        public enum MemoryProtection
        {
            Execute = 0x10,
            ExecuteRead = 0x20,
            ExecuteReadWrite = 0x40,
            ExecuteWriteCopy = 0x80,
            NoAccess = 0x01,
            ReadOnly = 0x02,
            ReadWrite = 0x04,
            WriteCopy = 0x08,
            GuardModifierflag = 0x100,
            NoCacheModifierflag = 0x200,
            WriteCombineModifierflag = 0x400
        }

        [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
        static extern IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress,
        uint dwSize, AllocationType flAllocationType, MemoryProtection flProtect);

        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern IntPtr OpenProcess(
        ProcessAccessFlags processAccess,
        bool bInheritHandle,
        int processId);

        [DllImport("kernel32.dll", SetLastError = true)]
        static extern bool ReadProcessMemory(
        IntPtr hProcess,
        IntPtr lpBaseAddress,
        [Out] byte[] lpBuffer,
        int dwSize,
        out IntPtr lpNumberOfBytesRead);

        public static IntPtr OpenProcess(Process proc, ProcessAccessFlags flags)
        {
            return OpenProcess(flags, false, proc.Id);
        }

        [DllImport("kernel32.dll", SetLastError = true)]
        static extern bool WriteProcessMemory(
        IntPtr hProcess,
        IntPtr lpBaseAddress,
        byte[] lpBuffer,
        int nSize,
        out IntPtr lpNumberOfBytesWritten);

        [DllImport("kernel32.dll", SetLastError = true)]
        static extern IntPtr CreateToolhelp32Snapshot(uint dwFlags, uint th32ProcessID);

        [DllImport("kernel32.dll")]
        static extern bool Process32First(IntPtr hSnapshot, ref PROCESSENTRY32 lppe);

        [DllImport("kernel32.dll")]
        static extern bool Process32Next(IntPtr hSnapshot, ref PROCESSENTRY32 lppe);
        #endregion
    }
}

Drawbacks

  1. One drawback of this is that you cannot use the returned int anymore. You have to dereference its value twice to get the actual data. Value -> Structure -> Actual Data
  2. Second drawback is the limitation of the integer value. If VirtualAllocEx on the parent returns an IntPtr bigger than Int32.MaxValue then the return process will fail. It will however always succeed if the parent application is a 32bit process or needs less than 2.1 GB of RAM.'
  3. Security plays a big role here. The child process must be allowed to write the parents memory. These flags must be set in the child process: PROCESS_VM_OPERATION, PROCESS_VM_READ, PROCESS_VM_WRITE.
    Luckily, this can be achieved by setting the Verb argument to "runas":
    C#
    var start=new ProcessStartInfo("TestCaller.exe"); 
    start.Verb = "runas"; 
    Process TestCalled = Process.Start(start);
  4. Your antivirus might not like one process writing to another (security).

If you find mistakes or have something to say, please leave a comment.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here