Taken from https://is.muni.cz/el/1433/jaro2010/PB167/um/cv5/undocumented_CreateProcess.pdf
libuv has utilized this to pass file descriptors to child process (remember that Windows doesn't have file descriptor concept):
- https://github.com/libuv/libuv/blob/47e0c5c575e92a25e0da10fc25b2732942c929f3/src/win/process.c#L1060
- https://github.com/libuv/libuv/blob/47e0c5c575e92a25e0da10fc25b2732942c929f3/src/win/process-stdio.c#L32
The last undocumented trick is quite different to the previous ones so I thought I'd save it until last. The STARTUPINFO structure contains two members, lpReserved2 and cbReserved2:
WORD cbReserved2;
LPBYTE lpReserved2;
These two members provide a mechanism for passing arbitrary amounts of data from one process to another, without having to call VirtualAllocEx / WriteProcessMemory. The cbReserved2 member is a 16bit integer and specifies thesize of the buffer pointed to by lpReserved2. This means that lpReserved2 canbe as big as 65535 bytes.
The example below demonstrates how to pass a buffer from one process to another. When process-B is executed by process-A, it will display a message box saying "Hello from Process A!":
Process A:
int main()
{
STARTUPINFO si = { sizeof(si) };
PROCESS_INFORMATION pi;
char buf[4444] = "Hello from Process A!";
// setup the buffer to pass to child process
si.lpReserved2 = buf;
si.cbReserved2 = sizeof(buf);
// create a new process with this data
if (CreateProcess(0, szExe, 0, 0, 0, 0, 0, 0, &si, &pi))
{
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
}
}
Process B:
int mainCRTStartup()
{
STARTUPINFO si = { sizeof(si) };
GetStartupInfo(&si);
// display what process A send us
MessageBox(NULL, si.lpReserved2, "Process B", MB_OK);
}
So far so good - we have a nice method for passing arbitrary binary data between applications, without having to use the command line. There is a problem though. In the example above process-B must be compiled with absolutely no C-runtime support. The reason is quite complicated but I will explain it now:
The Microsoft C runtime (including Visual Studio.NET) uses the lpReserved2 feature in it's implementation of the C functions: exec, system and spawn. When a new process is executed using these routines, the C-runtime has to be able to give the child process copies of it's open file handles (opened using fopen/open rather than CreateFile).
lpReserved2 is used as a mechanism to pass this file-handles information between programs compiled using MSVC. Before the exec/spawn runtime functions inevitably call CreateProcess, the lpReserved2 buffer is constructed using the following format:
DWORD count;
BYTE flags[count];
HANDLE handles[count];
// in memory these are layed out sequentially:
[ count ][ flags... ][ handles... ]
The first field in the lpReserved2 buffer is a 32bit count of the number of file-handles being passed. Next comes a BYTE-array of flags, one for each file-handle. These flags represent the file attributes that were used when the file was opened using fopen/open - i.e. read-only, write-append, text-mode, binary-mode etc. Immediately following the flags array is the HANDLE-array containing each open file. This array contains the actual file handle identifiers, again there are handle_count items in the array.
Any amount of data can follow this structure, up to a maximum of 65536 bytes. The cbReserved2 member must be set to the length (in bytes) of this structure.
The actual steps need to construct the lpReserved2 are as follows:
- Any currently open "runtime" file-handles are enumerated.
- Each underlying win32 HANDLE is marked as inheritable.
- The number of file-handles being passed between processes is stored as the first 32bit integer in lpReserved2.
- The attributes of each open file-handle are stored sequentially in the flags BYTE-array.
- The file-handle integers are stored sequentially in the handles 32bit int-array.
- Finally CreateProcess is called, with the bInheritHandles parameter set to TRUE.
It is at this stage that the C-runtime becomes important. When the child process executes, as part of it's initialization before main() is called, the I/O runtime support needs to be initialized. During this initialization the C-runtime calls GetStartupInfo and checks to see if an lpReserved2 buffer has been specified. If lpReserved2 is not NULL (i.e. it points to valid memory) then the C-runtime parses the contents of the buffer and extracts the file-handles contained within - this is so the file-handles will be available to the new process.
- Call GetStartupInfo and check if lpReserved2 points to valid data.
- Extract the first 32bit integer - this is the number of file-handles in the buffer.
- Intialize the current process's I/O state with this number of open files.
- Loop over the flags[] array in lpReserved2.
- Loop over the handles[] array, populating the open-file table.
The problem might now be apparent to the more astute readers. Any program compiled using Microsoft's C-runtime will check it's lpReserved2 when it first starts - it should come as no surprise that this probably represents 90% of the C/C++ Windows programs in existence. The lpReserved2 member may also be used by other compiler vendors for the same purpose.
Should you wish to use lpReserved2 in your own programs (using CreateProcess instead of spawn/exec) you will need to be careful because the lpReserved2 buffer must be properly constructed. Failure to do so will result in the child processes crashing, or at the very least becoming unstable - the reason being that the child process is expecting to find lpReserved2 in a particular format.
Getting around this problem is simple. Setting the first 4 bytes in lpReserved to zeros (nulls) indicates that the handle-arrays are empty, and any c-runtime startup code will simply skip this phase. Your arbitrary binary data can come immediately after this zero-marker. The code now looks like this:
Process A:
int main()
{
STARTUPINFO si = { sizeof(si) };
PROCESS_INFORMATION pi;
char buf[4444];
// construct the lpReserved2 buffer
*(DWORD *)buf = 0;
lstrcpy(buf + sizeof(DWORD), "Hello from Process A!");
// setup the buffer to pass to child process
si.lpReserved2 = buf;
si.cbReserved2 = sizeof(buf);
// create a new process with this data
if (CreateProcess(0, szExe, 0, 0, 0, 0, 0, 0, &si, &pi))
{
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
}
}
Process B:
int main()
{
STARTUPINFO si = { sizeof(si) };
GetStartupInfo(&si);
// display what process A sent us
if (si.lpReserved2 != NULL)
MessageBox(NULL, si.lpReserved2 + sizeof(DWORD), "Process B", MB_OK);
}
The example above can now be compiled without having to worry about C-runtime considerations.
Note: Apparently this method does not work under 64bit Windows Vista.