0

I'm trying to launch a child executable to run as the logged in user(written in golang) from a windows service running as SYSTEM. I am using the CreateProcessAsUser method to start the process. I've observed that the child process actually starts, but i'm unable to read the stdout of it. I've followed the steps described in https://learn.microsoft.com/en-us/windows/win32/procthread/creating-a-child-process-with-redirected-input-and-output to redirect the stdin and stdout of the child.

I have also set the permissions on the pipe to the Everyone SID, but still getting the same issue.

The parent process's code is as follows (after further edits based on suggestions in the comments)

#include "test.h"
#include <windows.h>
#include <winbase.h>
#include <userenv.h>
#include <tchar.h>
#include <wtsapi32.h>
#include <strsafe.h>
#include <processthreadsapi.h>

#define BUFSIZE 4096 
#define PROC_THREAD_ATTRIBUTE_HANDLE_LIST ( 2 | 0x00020000)
typedef struct _STARTUPINFOEXA {
    STARTUPINFOA StartupInfo;
    LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList;
} STARTUPINFOEXA, *LPSTARTUPINFOEXA;

void ReadAndHandleOutput(HANDLE hPipeRead, FILE* fp)
{
    CHAR lpBuffer[BUFSIZE];
    DWORD nBytesRead;
    DWORD bytesAvailable;
    DWORD bytesLeftInMsg;
    do
    {
        if (!PeekNamedPipe(hPipeRead, lpBuffer, sizeof(lpBuffer), &nBytesRead, &bytesAvailable, &bytesLeftInMsg))
        {
            if (GetLastError() == ERROR_BROKEN_PIPE) {
                fprintf(fp, "Broken Pipe \n");
                fflush(fp);
                break; // pipe done - normal exit path.
            }
            else {
                fprintf(fp, "ReadFileError %d\n", GetLastError()); // Something bad happened.
                fflush(fp);
            }
        }   
    } while(bytesLeftInMsg > 0);
    fprintf(fp, "GOT OUTPUT, %s -- %d -- %d -- %d\n", lpBuffer, nBytesRead, bytesAvailable, bytesLeftInMsg);
    fflush(fp);
}

bool LaunchProcess(char *process_path)
{
    FILE *fp = fopen("C:\\test.log", "a");
    DWORD SessionId = WTSGetActiveConsoleSessionId();
    if (SessionId == -1)  { // no-one logged in 
        fprintf(fp, "Session ID is -1\n");
        fclose(fp);
        return false;
    }

    HANDLE hToken;
    BOOL ok = WTSQueryUserToken(SessionId, &hToken);
    if (!ok) {
        fprintf(fp, "Unable to get the token for session id %d\n", SessionId);
        fclose(fp);
        return false;
    }

    void *environment = NULL;
    ok = CreateEnvironmentBlock(&environment, hToken, TRUE);

    if (!ok)
    {
        fprintf(fp, "Unable to create environment with session ID %d\n", SessionId);
        CloseHandle(hToken);
        fclose(fp);
        return false;
    }

    HANDLE hServerPipe = CreateNamedPipe("\\\\.\\Pipe\\test-pipe.em", PIPE_ACCESS_DUPLEX, PIPE_TYPE_BYTE|PIPE_READMODE_BYTE, 1, 0, 0, 0, 0);
    HANDLE hClientPipe;
    if (hServerPipe != INVALID_HANDLE_VALUE)
    {
        static SECURITY_ATTRIBUTES sa = { sizeof(sa), 0, TRUE };

        hClientPipe = CreateFile("\\\\.\\Pipe\\test-pipe.em", FILE_GENERIC_READ|FILE_GENERIC_WRITE, 0, &sa, OPEN_EXISTING, 0, 0);

        if (hClientPipe == INVALID_HANDLE_VALUE)
        {
            fprintf(fp, "Error creating client handle %d", GetLastError());
            CloseHandle(hServerPipe);
            return false;
        }
    } else {
        fprintf(fp, "Error creating server handle %d", GetLastError());
        return false;
    }

    STARTUPINFOEXA si = { { sizeof(si) } };
    PROCESS_INFORMATION pi;
    si.StartupInfo.dwFlags = STARTF_USESTDHANDLES;
    si.StartupInfo.hStdInput = si.StartupInfo.hStdOutput = si.StartupInfo.hStdError = hClientPipe;
    DWORD dwCreationFlags = CREATE_NO_WINDOW | CREATE_UNICODE_ENVIRONMENT;

    BOOL fInit = FALSE;
    SIZE_T Size;
    if (!InitializeProcThreadAttributeList(0, 1, 0, &Size) && GetLastError() == ERROR_INSUFFICIENT_BUFFER &&
            InitializeProcThreadAttributeList(si.lpAttributeList = (LPPROC_THREAD_ATTRIBUTE_LIST)alloca(Size), 1, 0, &Size))
    {
        fInit = TRUE;
        if (UpdateProcThreadAttribute(si.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, &si.StartupInfo.hStdError, sizeof(HANDLE), 0, 0)) {
                dwCreationFlags |= EXTENDED_STARTUPINFO_PRESENT;
        }
    }

    fprintf(fp, "calling the process %s\n", process_path);
    fflush(fp);
    ok = CreateProcessAsUser(hToken, NULL, process_path, NULL, NULL, TRUE, dwCreationFlags, environment, NULL, &si.StartupInfo, &pi);
    fprintf(fp, "Running process %s and %d with PID %d\n", process_path, ok, pi.dwProcessId);
    fflush(fp);
    if (!ok) {
        wchar_t buf[BUFSIZE];
        FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
               NULL, GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), 
               buf, (sizeof(buf) / sizeof(wchar_t)), NULL);

        fprintf(fp, "Failed to create processes as user - %d, %d, %S, %s\n", SessionId, GetLastError(), buf, process_path);
        fclose(fp);

        return false;
    } else {
        CloseHandle(pi.hThread);
        CloseHandle(pi.hProcess);
    }

    // Close pipe handles (do not continue to modify the parent).
    // You need to make sure that no handles to the write end of the
    // output pipe are maintained in this process or else the pipe will
    // not close when the child process exits and the ReadFile will hang.
    if (!CloseHandle(si.StartupInfo.hStdError)) {
        fprintf(fp, "CloseHandle - outputwrite Error %d\n", GetLastError());   
        fclose(fp);
        return false;    
    } 
    ReadAndHandleOutput(hServerPipe, fp);

    CloseHandle(hServerPipe);
    fclose(fp);
    return true;

Cleanup:
    fprintf(fp, "Something went wrong");
    fclose(fp);
    return false;
}

The child process's code is as follows

package main

import (
    "fmt"
    "os"
    "time"
)

func main() {
    os.Stdout.Write([]byte("Hello from the GO side..."))
    f, err := os.OpenFile("C:\\log.text", os.O_APPEND|os.O_WRONLY, 0600)
    if err != nil {
        panic(err)
    }

    defer f.Close()
    var output = fmt.Sprintf("I'm running as %d launched by %d", os.Getpid(), os.Getppid())
    if _, err = f.WriteString(output); err != nil {
        panic(err)
    }
    os.Stdout.Close()
}

I'm able to see the current PID and the parent's PID(the service) in the log.text file, However I keep getting a pipe broken error on the parent side.

Any pointers are appreciated

Tchinmai
  • 620
  • 1
  • 6
  • 20
  • 1
    Does the code work fine if you /don't/ change user ID? – Gem Taylor Jul 09 '19 at 15:57
  • 1
    Yes it does. So I suspect the problem is somewhere in the permissions for the pipe, I just don't know the right way to do it. – Tchinmai Jul 09 '19 at 16:22
  • Rather than using the Everyone SID in an ACL, have you tried setting a null DACL on the `SECURITY_DESCRIPTOR` that you give to `CreatePipe()`? That is the typical way to grant all access to everyone. `SetSecurityDescriptorDacl(pSD, TRUE, NULL, FALSE)` – Remy Lebeau Jul 09 '19 at 17:51
  • @RemyLebeau I just tried it out, and I'm still seeing the same issue - a BrokenPipe in the ```ReadAndHandleOutput``` method. – Tchinmai Jul 09 '19 at 19:11
  • The way you are managing handle inheritance is old-school. Use [`SetHandleInformation()`](https://learn.microsoft.com/en-us/windows/win32/api/handleapi/nf-handleapi-sethandleinformation) to control per-handle inheritance without having to duplicate handles, and [use `STARTUPINFOEX` with a `PROC_THREAD_ATTRIBUTE_HANDLE_LIST` attribute list](https://devblogs.microsoft.com/oldnewthing/20111216-00/?p=8873) to control which specific handles can be passed by `CreateProcessAsUser()` to the child. – Remy Lebeau Jul 09 '19 at 19:21
  • So, I'd be replacing ```DuplicateHandle()``` calls with ```SetHandleInformation()``` with ```dwFlags``` set to HANDLE_FLAG_INHERIT, and pass the Handles - ```hOutputWrite```, ```hInputRead```, ```hErrorWrite``` as an attribute list? – Tchinmai Jul 09 '19 at 19:44
  • security descriptor not play any role here at all, because you have already opened handles. so drop all code related to set *SD*. `ERROR_BROKEN_PIPE` mean that other end (to wich you was connect) of pipe is closed. not need create 2 pipe pairs (4 handles) - enough 1 pipe pair - 2 handles only. in place `GetCurrentSessionId` use `WTSGetActiveConsoleSessionId`.. – RbMm Jul 09 '19 at 23:51
  • *I suspect the problem is somewhere in the permissions for the pipe* absolute wrong suspect – RbMm Jul 10 '19 at 00:08
  • you have whole zoo of pipe handles - all this wrong and not need. not need use `CreatePipe` api (very bad design). create only 2 handles with `CreateNamedPipe` (`PIPE_ACCESS_DUPLEX`)+ `CreateFile`. you not need any duplicate or `SetHandleInformation` at all. just create client hand with inherited attribute and server without it. – RbMm Jul 10 '19 at 00:21
  • @RbMm Thanks for the clarification, is there an example implementation that you can link to? – Tchinmai Jul 10 '19 at 12:50
  • for exampe [1](https://stackoverflow.com/questions/49327749/how-to-run-a-batch-file-and-read-output/49330919#49330919) [2](https://stackoverflow.com/questions/46611207/createprocess-cmd-exe-read-write-pipes-deadlock/46613238#46613238) [3](https://stackoverflow.com/questions/60645/overlapped-i-o-on-anonymous-pipe/51448441#51448441) note that you not mandatory need use asynchronous io from server side, as i. can use and synhronous too. so 1.) create only 2 pipe handles. 2) set inherit attribute (on chile handle) already on create (so not need change it later). not need any SD – RbMm Jul 10 '19 at 13:15
  • @RbMm I have modified the code per your suggestions, and I see a different problem, the parent is unable to read anything in the stdout/stderr of the child. There are no errors either. – Tchinmai Jul 10 '19 at 19:18
  • *| STARTF_USESHOWWINDOW;* - remove this - faster of all your process just exit with this flag. `if (SessionId == -1)` must be check. `PIPE_ACCESS_DUPLEX|FILE_READ_DATA|FILE_WRITE_DATA` - error - `PIPE_ACCESS_DUPLEX` must be alone – RbMm Jul 11 '19 at 00:03
  • @RbMm still seeing the same issue after these changes. Unable to read anything from the child – Tchinmai Jul 11 '19 at 14:32
  • @Tchinmai - are you debug your code at all ? not need do this from service (you have no problem here). simply drop `WTSQueryUserToken` call and pass 0 in place `hToken` in `CreateProcessAsUser`. and **debug** . look are child is run. are it not exit just. *Unable to read anything from the child* - in sense ? which error ? and not use `PeekNamedPipe` too. – RbMm Jul 11 '19 at 15:02

0 Answers0