Page 1 of 1

Mouse Events are detected far less in a GOTO loop vs FOR /L loop

Posted: 27 Feb 2024 22:39
by Lowsun
Hey guys,

Recently I have been making a cmd extension that takes mouse and keyboard input implicitly into the variable CMDCMDLINE. I got inspiration when I saw this post viewtopic.php?f=3&t=4312 about CMDCMDLINE. It works well, except, when used in a GOTO loop, many mouse events seem missed / lag, but not the keyboard events. Here the exe code

Code: Select all

#include <stdio.h>
#include <stdlib.h>

#include "ntdll/ntdll.h"
#include <windows.h>

// compile   : doskey ds=cl radish.c "ntdll/ntdll_x64.lib" ^& test.bat
// ntdll lib : https://github.com/x64dbg/ScyllaHide/blob/master/3rdparty/ntdll/ntdll.h
// debug     : printf("Radish Failed with Error : %d", GetLastError());\

// Bugs :
// High CPU usage -> Sleep fixes it but will lag Mouse Events in GOTO loop
// Unfocus creates sticky keys FIXED
// Scroll Wheel doesn't end BY DESIGN

#define IF_ERR_EXIT(status) do {\
    if (!(status)) {\
        exit(EXIT_FAILURE);\
    }\
} while(0)

#define KEY_SET_MAX   (0xFE + 1)
#define KEY_BUF_MAX   49
#define NULL_TERM_LEN 1
#define SPACE_LEN     1
#define QUOTE_PAIR    2
#define ARG_NUM       3
#define INPUT_NUM     50
#define OUTPUT_BUFFER "%comspec%\\##########################\\..\\..\\cmd.exe /k " // 123.123.12.12.-123-123-123-123-123-123-123-123-123-123-123-123-

void set_con_mode(HANDLE std_in);
RTL_USER_PROCESS_PARAMETERS get_rtl_param(HANDLE con, PROCESS_BASIC_INFORMATION pbi);
wchar_t* get_buffer(HANDLE con, PROCESS_BASIC_INFORMATION pbi, size_t* max_len);

int
main(int argc, char* argv[]) {
    IF_ERR_EXIT(argc == ARG_NUM);
    char* com = calloc(strlen(OUTPUT_BUFFER) + strlen(argv[1]) + QUOTE_PAIR + SPACE_LEN + strlen(argv[2]) + NULL_TERM_LEN, sizeof(char));
    IF_ERR_EXIT(com);
    sprintf(com, "%s\"%s\" %s", OUTPUT_BUFFER, argv[1], argv[2]);

    STARTUPINFO si = {
        .cb = sizeof(si)
    };
    PROCESS_INFORMATION pi = {0};
    IF_ERR_EXIT(
        CreateProcess(
           NULL,
           com,
           NULL,           
           NULL,          
           FALSE,           
           CREATE_NEW_PROCESS_GROUP,         
           NULL, 
           NULL,         
           &si,           
           &pi
        )
    );
    
    free(com);
    Sleep(200); // make sure process starts first BEFORE querying info

    SetPriorityClass(GetCurrentProcess(), BELOW_NORMAL_PRIORITY_CLASS);
    PROCESS_BASIC_INFORMATION pbi = {0};
    NtQueryInformationProcess(pi.hProcess, ProcessBasicInformation, &pbi, sizeof(pbi), NULL);

    size_t max_len;
    wchar_t* buffer = get_buffer(pi.hProcess, pbi, &max_len);

    RTL_USER_PROCESS_PARAMETERS rtl = get_rtl_param(pi.hProcess, pbi);
    IF_ERR_EXIT(WriteProcessMemory(pi.hProcess, rtl.CommandLine.Buffer, buffer, sizeof(wchar_t) * max_len , NULL));

    HANDLE std_in = GetStdHandle(STD_INPUT_HANDLE);
    HANDLE std_out = GetStdHandle(STD_OUTPUT_HANDLE);

    set_con_mode(std_in);

    SHORT mouse_x = 0;
    SHORT mouse_y = 0;
    DWORD mouse_b = 0;
    BOOL focus = TRUE;

    BOOL key_set[KEY_SET_MAX] = {0};
    char key_buf[KEY_BUF_MAX + NULL_TERM_LEN] = {0};
    key_buf[0] = '-';
    
    int written = 0;
    INPUT_RECORD rec[INPUT_NUM] = {0};
    CreateFileA("RADISH_READY", 0, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);

    while (TRUE) {
        
        set_con_mode(std_in);
        DWORD num_event = 0;
        GetNumberOfConsoleInputEvents(std_in, &num_event);
        if (num_event) {
            DWORD num_read;
            ReadConsoleInputW(std_in, rec, INPUT_NUM, &num_read);
            for (int i = 0; i < num_read; i++) {
                if (rec[i].EventType == MOUSE_EVENT) {
                    mouse_x = rec[i].Event.MouseEvent.dwMousePosition.X;
                    mouse_y = rec[i].Event.MouseEvent.dwMousePosition.Y;
                    
                    CONSOLE_SCREEN_BUFFER_INFO con_info = {0};
                    GetConsoleScreenBufferInfo(std_out, &con_info);
                    SHORT h = con_info.srWindow.Bottom - con_info.srWindow.Top;
                    if (con_info.dwCursorPosition.Y >= h) {
                        mouse_y -= (con_info.dwCursorPosition.Y - h);
                    }                        
                   
                    if (rec[i].Event.MouseEvent.dwEventFlags == MOUSE_WHEELED) {
                        mouse_b = (short) HIWORD(rec[i].Event.MouseEvent.dwButtonState) / 120 * 6; // 6 or -6 for up / down scroll
                    } else {
                        mouse_b = rec[i].Event.MouseEvent.dwButtonState;
                    }
                    
                } else if (rec[i].EventType == KEY_EVENT) {
                    key_set[rec[i].Event.KeyEvent.wVirtualKeyCode] = rec[i].Event.KeyEvent.bKeyDown;
                } else if (rec[i].EventType == FOCUS_EVENT) {
                    focus = rec[i].Event.FocusEvent.bSetFocus;
                    if (!focus) {
                        for (int i = 0; i < KEY_SET_MAX; i++) {
                            key_set[i] = FALSE;
                        }
                    }
                }
            }
        }

        char* key_write = key_buf + 1; // 1 = '-'
        memset(key_write, 0, KEY_BUF_MAX);
        for (int i = 0; i < KEY_SET_MAX; i++) {
            if (key_set[i] == TRUE) {
                key_write += sprintf(key_write, "%d-", i);
                if (key_write - key_buf > KEY_BUF_MAX) {
                    break;
                }
            }
        }

        memset(buffer, 0, sizeof(wchar_t) * written);
        if ((key_write - key_buf) == 1) {
            written = swprintf(buffer, max_len, L"%d.%d.%d", mouse_x, mouse_y, mouse_b);
        } else {
            written = swprintf(buffer, max_len, L"%d.%d.%d.%hs", mouse_x, mouse_y, mouse_b, key_buf);
        }

        if (written > 0) {
            RTL_USER_PROCESS_PARAMETERS rtl = get_rtl_param(pi.hProcess, pbi);
            IF_ERR_EXIT(WriteProcessMemory(pi.hProcess, rtl.CommandLine.Buffer, buffer, sizeof(wchar_t) * (written + NULL_TERM_LEN), NULL));
        }

        Sleep(30);
    }
    
    free(buffer);
    return EXIT_SUCCESS;
}

void
set_con_mode(HANDLE std_in) {
    DWORD mode = 0;
    GetConsoleMode(std_in, &mode);
    SetConsoleMode(std_in, ENABLE_MOUSE_INPUT | ENABLE_EXTENDED_FLAGS | (mode & ~ENABLE_QUICK_EDIT_MODE));
}

wchar_t*
get_buffer(HANDLE con, PROCESS_BASIC_INFORMATION pbi, size_t* max_len) {
    RTL_USER_PROCESS_PARAMETERS rtl = get_rtl_param(con, pbi);
    *max_len = rtl.CommandLine.Length / sizeof(wchar_t);
    
    wchar_t* buffer = calloc(*max_len, sizeof(wchar_t));
    IF_ERR_EXIT(buffer);
    
    return buffer;
}

RTL_USER_PROCESS_PARAMETERS
get_rtl_param(HANDLE con, PROCESS_BASIC_INFORMATION pbi) {
    
    PEB peb = {0};
    ReadProcessMemory(con, pbi.PebBaseAddress, &peb, sizeof(peb), NULL);
    
    RTL_USER_PROCESS_PARAMETERS rtl = {0};
    IF_ERR_EXIT(ReadProcessMemory(con, peb.ProcessParameters, &rtl, sizeof(rtl), NULL));
    
    return rtl;
}
It works by getting the RTL_USE_PROCESS_PARAMETER of a CMD process it launches, and continually updates the Commandline parameter with the keyboard / mouse input. And since the CMDCMDLINE variable reads directly from this field, the variable is updated by itself implicitly. Here is the working Batch file I use to test

Code: Select all

@ECHO OFF
SETLOCAL ENABLEDELAYEDEXPANSION
(CHCP 65001)>NUL

IF not "%~1" == "" (
    GOTO :%~1
)
FOR /F %%A in ('ECHO PROMPT $E^| CMD') DO SET "ESC=%%A"

CALL :RADISH GAME

ECHO Finished

EXIT /B

:GAME
CLS
CALL :RADISH_WAIT

FOR /L %%Q in () DO (
    ECHO %ESC%[1;1HPress P to quit%ESC%[2;1H%ESC%[2KKEYS : [!CMDCMDLINE!]
    FOR /F "tokens=1-4 delims=." %%A in ("!CMDCMDLINE!") DO (
        SET "keys=%%D "
        IF not "!keys!" == "!keys:-80-=!" (
            %RADISH_END%
        )
    )

)


:RADISH
IF exist "RADISH_READY" (DEL /F /Q "RADISH_READY")
SET "RADISH_END=(TASKKILL /F /IM "RADISH.exe")>NUL & EXIT"
RADISH "%~nx0" %~1
GOTO :EOF
:RADISH_WAIT 
IF exist "RADISH_READY" (DEL /F /Q "RADISH_READY" & GOTO :EOF)
(TIMEOUT /T 1)>NUL
GOTO :RADISH_WAIT
However, when I replace the above loop with GOTO, the mouse events become laggy and unresponsive, but not the keyboard events. Like so

Code: Select all

@ECHO OFF
SETLOCAL ENABLEDELAYEDEXPANSION
(CHCP 65001)>NUL

IF not "%~1" == "" (
    GOTO :%~1
)
FOR /F %%A in ('ECHO PROMPT $E^| CMD') DO SET "ESC=%%A"

CALL :RADISH GAME

ECHO Finished

EXIT /B

:GAME
CLS
CALL :RADISH_WAIT

:G
ECHO %ESC%[1;1HPress P to quit%ESC%[2;1H%ESC%[2KKEYS : [!CMDCMDLINE!]
FOR /F "tokens=1-4 delims=." %%A in ("!CMDCMDLINE!") DO (
    SET "keys=%%D "
    IF not "!keys!" == "!keys:-80-=!" (
        %RADISH_END%
    )
)

GOTO :G


:RADISH
IF exist "RADISH_READY" (DEL /F /Q "RADISH_READY")
SET "RADISH_END=(TASKKILL /F /IM "RADISH.exe")>NUL & EXIT"
RADISH "%~nx0" %~1
GOTO :EOF
:RADISH_WAIT 
IF exist "RADISH_READY" (DEL /F /Q "RADISH_READY" & GOTO :EOF)
(TIMEOUT /T 1)>NUL
GOTO :RADISH_WAIT
I have attached the EXE file (64 bit) here, along with the working / not working test batch files. I was wondering if you guys had any idea what the difference between the two loops are, and why it would cause such a difference? If I take out the Sleep in the loop, it works, but CPU usage skyrockets. Not sure why that is. Perhaps the console mode is being reset? Thanks!

Re: Mouse Events are detected far less in a GOTO loop vs FOR /L loop

Posted: 01 Mar 2024 09:54
by jfl
I was wondering if you guys had any idea what the difference between the two loops are, and why it would cause such a difference?
We know that GOTO and CALL are implemented very inefficiently in the cmd.exe shell. There have been many discussions about this on this forum.
To get the best performance, it's necessary to avoid GOTO and CALL as much as possible.
Recently, there's been a whole thread discussing possible ways to do fast loops, and eventually break out of them, without using GOTO. Maybe you should try the new WHILE/DO and BREAK macros published in this post?

Re: Mouse Events are detected far less in a GOTO loop vs FOR /L loop

Posted: 01 Mar 2024 19:17
by Lowsun
jfl wrote:
01 Mar 2024 09:54
I was wondering if you guys had any idea what the difference between the two loops are, and why it would cause such a difference?
We know that GOTO and CALL are implemented very inefficiently in the cmd.exe shell. There have been many discussions about this on this forum.
To get the best performance, it's necessary to avoid GOTO and CALL as much as possible.
Recently, there's been a whole thread discussing possible ways to do fast loops, and eventually break out of them, without using GOTO. Maybe you should try the new WHILE/DO and BREAK macros published in this post?
Oh ye, I know, it's just the goal of this extension was to provide mouse / keyboard input through CMDCMDLINE variable regardless of the user provided code. It's just so strange to me that it runs fine except when the user has a GOTO loop. Therefore I was looking for a solution in the Winapi side of things. Do you have any ideas?

Thanks

Re: Mouse Events are detected far less in a GOTO loop vs FOR /L loop

Posted: 02 Mar 2024 11:13
by jfl
Lowsun wrote:
01 Mar 2024 19:17
I was looking for a solution in the Winapi side of things. Do you have any ideas?
Sorry, I don't.
And this forum is not the best place for questions about C programming

Re: Mouse Events are detected far less in a GOTO loop vs FOR /L loop

Posted: 02 Mar 2024 19:04
by Lowsun
Ah unfortunate. I was thinking perhaps this behaviour could be explained by some obscure CMD.exe behavioiur, which this forum is perfect for :D