Introduction
This article is going to show a way to return other values than integers from an application.
static int Main(string[] args)
{
return 0;
}
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:
[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:
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr OpenProcess(int dwDesiredAccess, bool bInheritHandle, int dwProcessId);
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:
[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.
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:
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:
[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.
[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.
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
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>();
}
}
static int Main(string[] args)
{
Process.GetCurrentProcess().Exit("Any string or class here");
}
Process A
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";
Process TestCalled = Process.Start(start);
TestCalled.WaitForExit();
int exit = TestCalled.ExitCode;
var MyString = TestCalled.TranslateExitCode<string>(TestCalled.ExitCode);
}
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:
using System.ComponentModel;
using System.IO;
using System.Runtime.InteropServices;
using System.Runtime.Serialization.Formatters.Binary;
namespace System.Diagnostics
{
public static class ProcessReturnHelper
{
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);
}
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);
}
}
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();
}
}
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");
}
return Data;
}
public static IntPtr WriteMemory(this Process process, byte[] Data)
{
IntPtr Destination = process.Alloc(Data.Length);
process.OverWriteMemory(Destination, Data);
return Destination;
}
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");
}
}
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
- 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 - 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.' - 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
":
var start=new ProcessStartInfo("TestCaller.exe");
start.Verb = "runas";
Process TestCalled = Process.Start(start);
- Your antivirus might not like one process writing to another (security).
If you find mistakes or have something to say, please leave a comment.