process.communicate and getche() fails

2019-07-03 16:28发布

问题:

I am attempting to automate the execution of an interactive command line tool written in C++.

When launched, the binary waits for the letter S, Q, or P (Status, Quit, or Pause). It uses a nonstandard msvcrt function "getche" to acquire the key stroke (instead of a gets() for example) without the user having to hit enter.

I tried communicating with the process in the standard way (writing to stdin and using process.communicate[]) but it doesn't get the input. After a few hours of trying different things I created two small sample projects in Visual Studio to replicate the issue and make sure I am sane(ish).

This is the python script used to call the binary:

import subprocess
import time

cmd = ["test-getch.exe"]
process = subprocess.Popen(cmd, stderr = subprocess.PIPE, stdin = subprocess.PIPE, stdout = subprocess.PIPE)
i = process.stdin
#msvcrt.ungetch('s')
i.write("S\n")
print process.communicate()[0]
i.close()
time.sleep(3)
print "DONE"

These are the two binaries. This first one I can communicate with:

#include "stdafx.h"
#include <conio.h>


int _tmain(int argc, _TCHAR* argv[])
{
    char response [2];
    printf("Enter \"s\":\n");
    gets(response);
    printf("You entered %s", response);
    return 0;
}

This one I can't communicate with:

#include "stdafx.h"
#include <conio.h>


int _tmain(int argc, _TCHAR* argv[])
{
    int response;
    printf("Enter \"a\":\n");
    response = getche();
    printf("You entered %c", response);
    return 0;
}

It appears that getche() doesn't listen on stdin and probably listens for some kind of keyboard event. Anyone know how to deal with this?

EDIT: I should also mention I discovered the method of capturing input using IDA Pro. I did not write the original binary that I am attempting to automate. It is a closed source tool so I have no way of re-writing how it accepts input without patching the binary.

I've actually chosen a rather insane solution that works... I know pydbg quite well and it seems that attaching to the process and calling the functions I need via process instrumentation works. It's totally overkill but I can detach from the process afterwards. and have it run normally.

[1] Pydbg: http://pedram.redhive.com/PyDbg/docs/

回答1:

getche reads from the console, not from standard input. If your Python process is running in a console window, then your subprocess will still try reading input from that same console, not the pipe it's being passed in as standard input.

It might be possible to create another invisible console window, attach it to the subprocess, and feed it input, but that's very complicated and error-prone.

I'd recommend instead just rewriting your program to only read from standard input instead of using getche(). If you really want to have it react to keystrokes without requiring the user to press Enter, then I'd suggest having it change its behavior depending on whether or not standard input is coming from a terminal. If it is, use getche, and if not, just read directly from stdin. You can test this using _isatty (or the POSIX-equivalent isatty; for some reason, Microsoft has decided to deprecate the POSIX name in their runtime). For example:

int ReadChar()
{
    if(_isatty(0))
    {
        // stdin is a terminal
        return _getche();
    }
    else
    {
        // stdin is not a terminal
        return getchar();
    }
}


回答2:

Adam Rosenfield's answer is a sensible approach if you can modify the behavior of the called program. Otherwise, if you really need to write to the console input buffer, try PyWin32's win32console module. That said, I'm not sure how to get the character echo part working correctly when stdout is piped. It ends up printing to the start of the line.

C:

#include <stdio.h>

int main(int argc, char *argv[]) {
    int response;
    printf("Enter \"a\": ");
    response = getche();
    printf("\nYou entered \"%c\" ", response);
    return 0;
}

/* gcc test_getch.c -o test_getch.exe */

Python:

import subprocess
import win32console

def make_buf(c):
    buf = win32console.PyINPUT_RECORDType(win32console.KEY_EVENT)
    buf.KeyDown = 1
    buf.RepeatCount = 1
    buf.Char = c
    return buf

con_in = win32console.GetStdHandle(win32console.STD_INPUT_HANDLE)

cmd = ["test_getch.exe"]
p = subprocess.Popen(cmd, stdin=subprocess.PIPE)

buf = make_buf('a')
con_in.WriteConsoleInput([buf])