You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
BOOLIsConsoleHandle(HANDLEhHandle){DWORDdwMode;/* Check whether the handle may be that of a console... */if((GetFileType(hHandle)&~FILE_TYPE_REMOTE)!=FILE_TYPE_CHAR)returnFALSE;/* * It may be. Perform another test... The idea comes from the * MSDN description of the WriteConsole API: * * "WriteConsole fails if it is used with a standard handle * that is redirected to a file. If an application processes * multilingual output that can be redirected, determine whether * the output handle is a console handle (one method is to call * the GetConsoleMode function and check whether it succeeds). * If the handle is a console handle, call WriteConsole. If the * handle is not a console handle, the output is redirected and * you should call WriteFile to perform the I/O." */returnGetConsoleMode(hHandle,&dwMode);}
Privexec 的内幕(一)标准输出原理与彩色输出实现
https://ift.tt/RxCJlcm
前言
Privexec 是笔者借鉴远景好友 MouriNaruto 的 NSudo 而开发的一个提权或者降权执行进程的工具。其中 wsudo 是 Privexec 的命令行版本。
在 wsudo 中,笔者使用了 Privexec.Console 提供彩色输出,截图如下:
本文将讲述标准输出是如何输出到控制台的,以及怎样在 Windows 中实现同时支持标准控制台和 MSYS2 Cygwin 终端模拟器以及 VT 模式的控制台彩色输出。
关于标准输出
大部分编程语言的入门从
Helloworld
开始,也就是将文本Helloworld
输出到标准输出。在 C++ 中使用std::cout
,在 C 中使用printf
以及在 C# 中使用Console.Write
。进程启动时,操作系统或者父进程会设置好进程的标准输出1。默认情况下,标准输出设备是控制台 console
或者是终端 tty
当然在启动进程前,可以将标准输出重定向到管道 (Pipe/Named Pipe, Pipe/FIFO)
,文件
而在 Unix like 系统中,还可以将输出重定向到socket
等其他 Unix 文件。在 Windows 上,如果要将 IO 重定向到 socket 需要使用WSASocket
创建 socket,且不要使用WSA_FLAG_OVERLAPPED
标志。输出的设备或者文件存在多样性,对于 CRT 而言,标准输出的实现就要兼顾这些设备,通常来说,操作系统会提供
WriteFile
write
这样的 API 或者系统调用支持输出,一般来说,printf 这样的函数也是使用这样的 API 或者系统调用实现。这些函数的输出优先考虑的是本机默认编码,比如 Unix 上,一般都是 UTF-8,对于兼容性大户 Windows 来说,虽然内部编码都是 UTF-16 但是输出到文件时,任然优先选择的是本机代码页,比如在简体中文系统中是,代码页也就是 936。Printf 的心路历程
在以前我曾经思考过
printf
是如何实现的,很多开发者在开始也有同样的疑惑,在知乎上,就有人提问:printf()等系统库函数是如何实现的? ,在这个问题下,有很多人回复了,有兴趣的用户可以看一下;在 Unix 的 CRT 中,printf 的调用历程在这篇文章中有详细介绍:Where the printf() Rubber Meets the Road
在 Windows 10 中,新增了
Universal CRT (UCRT)
CRT Library Features,Introducing the Universal CRT,与之前的 Visual C++ CRT 有了很大的不同,全部代码使用 C++11 重构,不用疑惑,正是使用 C++11 实现 C Runtime。笔者对 printf 的分析也是基于 ucrt 。Visual C++ 会将 CRT/C++ STL 源码一同发布(没有构建文件),在 Visual Studio 的安装目录下的
VC\crt\src
,而UCRT
源码则在%ProgramFiles(x86)%Windows Kits\10\Source\$BuildVersion\ucrt
在 UCRT 中 printf 是个内联函数,调用了
_vfprintf_l
,_vfprintf_l
也是内联 的,它调用了__stdio_common_vfprintf
。在 ucrt 源码路径stdio\output.cpp
中__stdio_common_vfprintf
调用了模板函数common_vfprintf
,而common_vfprintf
则在内部调用了模板类output_processor
,output_processor
使用了输出适配器模板类stream_output_adapter
File stream 的写入流程是
write_strings
->write_string_impl
->write_character
->write_character_without_count_update
,然后是char_traits::puttc_nolock
。char_traits::puttch_nolock
实际上是通过__acrt_stdio_char_traits
__crt_char_traits
定义的静态成员函数,,printf 对应的是_fputc_nolock
。_fputc_nolock
和fputc
类似,实际上fputc
也会调用它,在_fputc_nolock
中调用了__acrt_stdio_flush_and_write_narrow_nolock
,在源码stdio/_flsbuf.cpp
中,__acrt_stdio_flush_and_write_narrow_nolock
又调用了common_flush_and_write_nolock<char>
往下一步走会调用
write_buffer_nolock
->_write
->_write_nolock(lowio/write.cpp)
然后根据不同设备和字符类型,
_write_nolock
会调用:最后终究要调用
WriteFile
,对于不需要缓冲区的文件读写为什么不直接使用WriteFile
?值得一提的是,在 Windows 中,如果使用 FILE 读写文件,尽量使用
rb
wb
之类的标志,显示的指定文件类型是binary
, 否则自动添加CR
就不好了。通过源码,我们还知道 UTF-16 或者 UTF-8 一般还是需要转换成 Ansi 才能输出到控制台。
UCRT 还提供了
_cputs
_cprintf
_cputws
_cwprintf
这样的函数,这些函数处理流程类似但要简单的多,output_processor
的输出适配器是console_output_adapter
,无论是字符类型是 wchar_t 还是 char 最终都会调用_putwch_nolock
及__dcrt_write_console_w
,最后使用WriteConsoleW
写入到控制台。_cwprintf
与wprintf
相比,输出 Unicode 字符要容易的多,不过,在使用标准输出的时候,你不能假定程序一定拥有控制台。WriteConsole 内部原理
虽然在 Windows/ReactOS 中,CRT 写入到标准输出的使用了
WriteFile
,WriteFile 是如何写到控制台的?在
ReactOS
源码中,WriteFile 将检查hFile
其值是否为STD_INPUT_HANDLE
,STD_OUTPUT_HANDLE
,STD_ERROR_HANDLE
如果是就从 PEB 中获得对应的控制台句柄,否则使用句柄hFile
原本的值,然后就判断是否是控制台句柄,如果是控制台,则调用WriteConsoleA
,对于其他类型文件会直接调用NtWriteFile
。将
STD_*_HANDLE
转换为 Windows 内核对象:判断是否是控制台文件:
或者
有兴趣的可以参阅 ReactOS WriteFile 源码: WriteFile to Console
对于控制台句柄,CloseHandle,ReadFile,CreateFile ,以及 WriteFile 都要单独的调用对象的控制台 API。比如说,如果文件名是
CON
,CONOUT$
,CONIN$
,\\.\CON
时就会使用OpenConsoleW
打开控制台。然后返回控制台的句柄。WriteConsoleA 又是如何写入到图形界面呢?在 Windows Technet 有两幅图分别介绍了 Vista 以前的控制台结构和 Windows 7 的控制台架构 Windows 7 / Windows Server 2008 R2: Console Host
在 Windows 7 以前,WriteConsole 通过 LPC 与 CSRSS(Client Server Runtime Process) 通信:
由于CSRSS 以
Local System
权限运行,这样的逻辑容易导致 Shatter attack,于是在 Windows 7 中出现了新的Console Host
:在这种架构中,WriteConsole LPC 调用将控制台消息发送到了一个 Conhost 宿主进程,这个进程是在 CreateProcess 中自动创建的。
在 ReactOS 中,依然使用的是
CsrCaptureMessageBuffer
将数据发送到 CSRSS。源码在这里: WriteConsoleReactOS 文档:ReactOS
WriteFile 输出到控制台时,实际调用的是 WriteConsoleA,在前面我们还知道使用 wprintf 时,CRT 会将文本内容转换成 Ansi(Codepage) 然后再使用 WriteFile 写入到控制台窗口。
我们知道,绘制字符的时候,ANSI 文本最终将会转换成 UTF-16LE 文本,然后经 DrawTextExW 或者其他函数绘制出来,如果使用了 wprintf 这样的函数,势必会经过两次转换 UTF16->CodePage->UTF16,在 Unicode 中存在的字符不一定存在于代码页中,这就导致文本在转换编码的时候发生丢失,输出到控制台时,可能是乱码或者干脆截断了。所以在 Windows 控制台中, Unicode 编码,特别是 emoji 还是不要妄想通过 wprintf 输出。
在 Windows 中,内码是 Unicode,而控制台也支持使用
WriteConsoleW
这样的 API 输出文本,如果我们直接使用WriteConsoleW
就可以避免出现字符无法呈现或者乱码的问题了。如果控制台的图形对各种字体字符支持更好,这个 API 也就能够输出彩色字符或者更多的 Emoji。遗憾的是,目前 Console 字体渲染的改进任然在计划中,暂时不支持 Emoji 和各种特殊字体。控制台彩色输出
讲了这么长一段,该讲如何实现彩色输出了,首先,我们要知道 Windows 控制台 API 是支持颜色输出的,不过,这些 API 仅支持 16 色输出。 在 .Net Core corefx 有如下一个枚举定义了控制台基本的颜色:
在 C++ 中,如果在
WriteConsoleW
调用之前使用了SetConsoleTextAttribute
设置输出格式,那么就能输出带有上述颜色的文本内容了。在 Privexec.Console 中,使用控制台 API 输出颜色代码如下:在这里,我们选择的是
STD_OUTPUT_HANDLE
,使用&0x0F
或者&0xF0
的目的是不修改原有的背景色或者前景色,第二次调用SetConsoleTextAttribute
的目的是恢复控制台原有的颜色。终端模拟器彩色输出
在 Windows 上,还有 Cygwin 和 MSYS2 MSYS 这样的模拟 Unix 的环境。wsudo 对其支持也非常有必要。 这些环境启动进程往往是通过管道通信,这个时候,我们可以判断是否是终端还是控制台。
如果使用 GetFileType(hConsole) 得到的文件类型不是
FILE_TYPE_CHAR
我们就可以确定不是控制台,并且如果不支持GetConsoleMode
函数,也要视其不是控制台。核心代码如下:
这些终端环境基本上将文本视为 UTF8 编码,为了让文字正常显示,我们需要将其转换为 UTF8。经过测试,中文都能正常显示。
VT 模式颜色输出
在 Windows 10 中,新增了 Windows Subsystem for Linux ,可以通过 Bash 命令启动终端运行 Linux 程序,Windows 控制台还增加了 VT 模式 Console Virtual Terminal Sequences,并且支持24-Bit 颜色:24-bit Color in the Windows Console!,这意味着,可以像 Linux 一样在 printf 中添加转义字符控制颜色输出。 在 Github 中也有 Issues 讨论: support 256 color 笔者在开发时发现 WriteConsoleW 也支持 VT 模式,对于也添加了代码支持 VT 模式:
由于 VT 模式支持 256 色,这里还增加了
PrintConsole
模板函数,支持用户自定义输出多一些色彩。输出函数自动选择
在不考虑 freopen 这样的重新设置标准输出输出的情况下,Privexec.Console 使用如下代码支持自动选择不同的输出函数
使用
console::Print
不用担心乱码和颜色问题,在这几个主流的环境中都能正常显示(包括 ConEmu)。Print 使用的完全是 wchar_t。其他
很欣慰的是 Windows 控制台团队在 Windows 10 开发之处就在不断改进控制台: 比如 Console Improvements in the Windows 10 Technical Preview
还有计划中的 Emoji 支持: Add emoji support to Windows Console
以及基于 DirectWrite 改进控制台字体渲染的计划: UTF-8 rendering woes
当然 ConEmu 也有计划使用 DirectWrite 改进其渲染。
不过遗憾的是,Mintty 的开发者并不认为有使用 DirectWrite 改进渲染的必要。
基于 Rust 的跨平台 GPU 终端 Alacritty - A cross-platform, GPU-accelerated terminal emulator 也计划在 1.0 时对 Windows 提供支持,字体渲染也有 DirectWrite 的身影。
Privexec.Console 官方并不会支持 Windows 10 以前的版本,毕竟作者精力有限。
备注
/dev/null
类似,但启动的 CUI 子进程默认下依然有控制台窗口I feedback.
Let me know what you think of this article on twitter @sinopre!
via Charlie's Rethinking
October 25, 2023 at 05:23PM
The text was updated successfully, but these errors were encountered: