Creating a child process with redirected IO
In a recent experiment, I sought to interact with a hollowed process in a manner similar to a conventional console application. Although there is an example on MSDN for creating a child process with redirected I/O, it does not create the interactive session I so desired. In this snippet I will demonstrate how to create a child process with interactive I/O
To accomplish this we need to create two pipes which that act as stdout and stdin for the spawned process. It is necessary to mark ChildStdoutR
and ChildStdinW
as non-inheritable, as the child process does not need to read its output or write to its input
HANDLE ChildStdinR { NULL };
HANDLE ChildStdinW { NULL };
HANDLE ChildStdoutR{ NULL };
HANDLE ChildStdoutW{ NULL };
BOOL CreatePipes()
{
SECURITY_ATTRIBUTES PipeAttributes = { sizeof(SECURITY_ATTRIBUTES), NULL, TRUE };
if (!CreatePipe(&ChildStdoutR, &ChildStdoutW, &PipeAttributes, NULL))
{
return FALSE;
}
if (!SetHandleInformation(ChildStdoutR, HANDLE_FLAG_INHERIT, NULL))
{
return FALSE;
}
if (!CreatePipe(&ChildStdinR, &ChildStdinW, &PipeAttributes, NULL))
{
return FALSE;
}
if (!SetHandleInformation(ChildStdinW, HANDLE_FLAG_INHERIT, NULL))
{
return FALSE;
}
if (!ChildStdinR || !ChildStdinW || !ChildStdoutR || !ChildStdoutW)
{
return FALSE;
}
return TRUE;
}
Once the pipes have been successfully initialised, the next step is to create the desired process. In order for the process to use the newly created pipes, it is necessary to use the STARTF_USESTDHANDLES
flag and specify the standard input/output/error handle in the appropriate members of the STARTUPINFO
structure: hStdError
, hStdInput
and hStdInput
. In addition, the handles for ChildStdoutW
and ChildStdinR
are closed, as there is no utility in writing to the output or reading the input of the created process
STARTUPINFOW StartupInfo{};
StartupInfo.cb = sizeof(STARTUPINFOW);
StartupInfo.hStdError = StartupInfo.hStdOutput = ChildStdoutW;
StartupInfo.hStdInput = ChildStdinR;
StartupInfo.wShowWindow = SW_HIDE;
StartupInfo.dwFlags |= (STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW);
if (!CreateProcessW(NULL, CommandLine, NULL, NULL, TRUE, CREATE_NEW_CONSOLE, NULL, NULL, &StartupInfo, *HollowedProcess))
{
return FALSE;
}
CloseHandle(ChildStdoutW);
CloseHandle(ChildStdinR);
return TRUE;
In order to read the output of the child process, a function has been designed to redirect ChildStdout
to ParentStdout
.
VOID ReadFromPipe()
{
HANDLE ParentStdout{ GetStdHandle(STD_OUTPUT_HANDLE) };
while (TRUE)
{
CHAR Buffer[BUFFER_SIZE];
DWORD BytesRead{};
if (!ReadFile(ChildStdoutR, Buffer, BUFFER_SIZE, &BytesRead, NULL))
{
break;
}
DWORD BytesWritten{};
if (!WriteFile(ParentStdout, Buffer, BytesRead, &BytesWritten, NULL))
{
break;
}
}
}
Input redirection is achieved in a similar way, using std::getline
to read from the console and WriteFile
to send the input to the ChildStdin
pipe.
VOID WriteToPipe()
{
while (TRUE)
{
std::string Command;
std::getline(std::cin, Command);
Command += '\n';
DWORD BytesWritten{};
if (!WriteFile(ChildStdinW, Command.c_str(), Command.length(), &BytesWritten, NULL))
{
break;
}
}
}
Given the need for seamless interaction, two threads are created to execute the above functions
HANDLE ReadPipeThread { CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ReadFromPipe, NULL, 0, NULL) };
HANDLE WritePipeThread{ CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)WriteToPipe, NULL, 0, NULL) };
if (ReadPipeThread == NULL || WritePipeThread == NULL)
{
return EXIT_FAILURE;
}
WaitForSingleObject(ReadPipeThread, INFINITE);
The main thread will wait for the thread responsible for reading from the ChildStdout
pipe to exit. At this point, it is possible to interact with the child process in the same way as with any console application