windows shell

Understanding Windows Console Host

If you open cmd.exe or powershell.exe in Windows, you will always find conhost.exe alongside them. As a matter of fact, conhost.exe has been around for more than a decade. Every (console based) Windows program has a "console" with them, for example when you compile and run a "command line" program, it brings up a black console window.

For conhost.exe specifically, there's no documentation. If you need to customize console/terminal settings, like you do in Linux/MacOS, you need to right click on the title bar and choose Properties, and the user settings are saved in registry or .lnk file that's associated with your shell program.

If no user settings are present or requested, for example when launched directly via conhost.exe cmd.exe, default settings are used, which can be found in registry under HKCU\Console.

If we are going to implement a remote Windows shell and get all the features provided by Console Host, essentially we will need to match the console window that lives alongside our shell (for example cmd.exe), meaning remotely our shell should have the same buffer/window size as the local one, and pipe conhost.exe's IO with our network connection to get whatever displayed in the console window and put it on our remote terminal.

I suggest read Microsoft's blog post for Windows Console Host to get a detailed understanding of Console Host.

Setting Name Type Description
FontSize Coordinate (REG_DWORD) Size of font in pixels
FontFamily REG_DWORD GDI Font family
ScreenBufferSize Coordinate (REG_DWORD) Size of the screen buffer in WxH characters
CursorSize REG_DWORD Cursor height as percentage of a single character
WindowSize Coordinate (REG_DWORD) Initial size of the window in WxH characters
WindowPosition Coordinate (REG_DWORD) Initial position of the window in WxH pixels (if not set, use auto-positioning)
WindowAlpha REG_DWORD Opacity of the window (valid range: 0x4D-0xFF)
ScreenColors REG_DWORD Default foreground and background colors
PopupColors REG_DWORD FG and BG colors used when displaying a popup window (e.g. when F2 is pressed in CMD.exe)
QuickEdit REG_DWORD Whether QuickEdit is on by default or not
FaceName REG_SZ Name of font to use (or __DefaultTTFont__, which defaults to whichever font is deemed most appropriate for your codepage)
FontWeight REG_DWORD GDI font weight
InsertMode REG_DWORD Whether Insert mode is on by default or not
HistoryBufferSize REG_DWORD Number of history entries to retain
NumberOfHistoryBuffers REG_DWORD Number of history buffers to retain
HistoryNoDup REG_DWORD Whether to retain duplicate history entries or not
ColorTable%% REG_DWORD For each of the 16 colors in the palette, the RGB value of the color to use
ExtendedEditKey REG_DWORD Whether to allow the use of extended edit keys or not
WordDelimiters REG_SZ A list of characters that are considered as delimiting words
TrimLeadingZeros REG_DWORD Whether to remove zeroes from the beginning of a selected string on copy (e.g. 00000001 becomes 1)
EnableColorSelection REG_DWORD Whether to allow selection colorization or not
ScrollScale REG_DWORD How many lines to scroll when using SHIFT / Scroll Wheel
CodePage REG_DWORD The default codepage to use
ForceV2 REG_DWORD Whether to use the improved version of the Windows Console Host
LineSelection* REG_DWORD Whether to use wrapped text selection
FilterOnPaste* REG_DWORD Whether to replace characters on paste (e.g. Word “smart quotes” are replaced with regular quotes)
LineWrap REG_DWORD Whether to have the Windows Console Host break long lines into multiple rows
CtrlKeyShortcutsDisabled REG_DWORD Disables new control key shortcuts
AllowAltF4Close REG_DWORD Allows the user to disable the Alt-F4 hotkey
VirtualTerminalLevel REG_DWORD The level of VT support provided by the Windows Console Host

Not easy to understand at first, and Microsoft gives no further explanation.

I will tell you how this works:

Let's take FontSize for example, if you open registry editor and get the value, it will be a REG_DWORD or say double word integer, why FontSize can be this huge???

Well it's actually not, considering Microsoft explains in the table:

Coordinate, Size of font in pixels

If you get a value 0x100000, you need to split it into two since it's a "Coordinate" type (I don't know why since the lower 4 bytes are always zero), that is, you get 0x10 and 0x0

0x10 is 16, which can be found in default console settings, font size is 16 indeed, but why in pixels?

I did some calculation with different window size and font size, and I can confirm that 16 as font size means 1 character takes 16 pixels in height, and 16/2 = 8 pixels in width, regardless of font family.

The same applies to WindowsSize, except it's in characters.

Dynamically Ajust Window Size

If you use a remote shell on Linux, you always need to ajust your local terminal size to match the remote one, Linux provides TIOCSWINSZ to do that.

Things on Windows will be much harder, but I can assure you it works the same way.

If you resize the console window and view it's window size change in Process Hacker, you will see ScreenBufferSize changing accordingly.

find window

You won't be able to use ScreenBufferSize since the console belongs to another process, my method is to use SetWindPos to resize the whole console window, just like you would do if it's a local console.

To use SetWindPos, you have to know what size (in pixels) you will need, including the window title bar, scroll bar, and even drop-shadow.

Firstly, since we are launching the shell via conhost.exe cmd.exe, it will use default settings which can be found in the registry, so we read the window size (in characters), and font size (in pixels), then we do some math and get current window size (in pixels), note that this size includes only client rectangle, see the screenshot to get a view of it.

To get the whole size of the Console window, use GetWindowRect, then you can calculate the extra width/height that will be used later.

Remember, you are trying to resize in window so its buffer size (in characters) becomes what you need, with all the above calculations, you should be able to get the exact size in pixels, and call SetWindPos to resize the window, so the buffer size updates as well.

Here's my approach in Go:

// SetCosoleWinsize resize main window of given console process
// w/h: width/height in characters
// window position resets to 0, 0 (pixel)
func SetCosoleWinsize(pid, w, h int) {
    whandle, err := GetWindowHandleByPID(pid, true)
    if err != nil {
        log.Printf("SetWinsize: %v", err)
        return
    }
    // read default font size from registry
    console_reg_key, err := registry.OpenKey(registry.CURRENT_USER, "Console", registry.QUERY_VALUE)
    if err != nil {
        log.Printf("SetCosoleWinsize: %v", err)
        return
    }
    defer console_reg_key.Close()
    font_size_val, _, err := console_reg_key.GetIntegerValue("FontSize")
    if err != nil {
        log.Printf("SetConsoleWinSize: query fontsize: %v", err)
        return
    }
    font_size := int(font_size_val >> 16) // font height in pixels, width = h/2
    log.Printf("Default font size of console host is %d (0x%x), parsed from 0x%x",
        font_size, font_size, font_size_val)
    // what size in pixels we need
    w_px := w * font_size / 2
    h_px := h * font_size

    if ConsoleExtraHeight == 0 && ConsoleExtraWidth == 0 {
        // Get default window size
        now_size, _, err := console_reg_key.GetIntegerValue("WindowSize")
        if err != nil {
            log.Printf("window size: %v", err)
            return
        }
        // in chars
        default_width := int(now_size & 0xffff)
        default_height := int(now_size >> 16)
        // in pixels
        default_w_px := default_width * font_size / 2
        default_h_px := default_height * font_size
        log.Printf("Default window (client rectangle) is %dx%d (chars) or %dx%d (pixels)",
            default_width, default_height,
            default_w_px, default_h_px)
        // window size in pixels, including title bar and frame
        now_rect := w32.GetWindowRect(whandle)
        now_w_px := int(now_rect.Width())
        now_h_px := int(now_rect.Height())
        if now_h_px <= 0 || now_w_px <= 0 {
            log.Printf("Now window (normal rectangle) size is %dx%d, aborting", now_w_px, now_h_px)
            return
        }
        // calculate extra width and height
        ConsoleExtraHeight = now_h_px - default_h_px
        ConsoleExtraWidth = now_w_px - default_w_px
        if ConsoleExtraWidth <= 0 || ConsoleExtraHeight <= 0 {
            log.Printf("Extra width %d, extra height %d, aborting", ConsoleExtraWidth, ConsoleExtraHeight)
            return
        }

    }
    w_px = w_px + ConsoleExtraWidth
    h_px = h_px + ConsoleExtraHeight

    // set window size in pixels
    if w32.SetWindowPos(whandle, whandle, 0, 0, w_px, h_px, w32.SWP_NOMOVE|w32.SWP_NOZORDER) {
        log.Printf("Window (0x%x) of %d has been resized to %dx%d (chars) or %dx%d (pixels)",
            whandle, pid, w, h, w_px, h_px)
    }
}

Comments

comments powered by Disqus