Skip to main content

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 about 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 will 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 utilise the newly created pipes, it is necessary to use the STARTF_USESTDHANDLES flag and specify the standard input/output/error handle in the respective members of the STARTUPINFO structure: hStdError, hStdInput and hStdInput. Moreover, 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 child process's output, a function was 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;
}
}
}

The redirection of input is achieved in a similar manner, utilising std::getline to read from the console and WriteFile to transmit 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;
}
}
}

In view of the necessity for seamless interaction, two threads are created to run the aforementioned 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 await the termination of the thread responsible for reading from the ChildStdout pipe. At this point, it is possible to interact with the child process in the same way as with any console application