MessageBox深入研究

MessageBox系列函数基本信息

MessageBox是最简单的图形界面交互API之一,只需要指定标题、正文、样式就可以弹出一个简单的对话框,而不需要指定消息处理例程,也不需要消息循环。然而Windows是一个复杂的操作系统,绝大多数的API的功能不可能简单实现,MessageBox也不例外。实际上,这个API内部大有文章。

MessageBox有以下几个版本:

  • MessageBoxA, MessageBoxW;
  • MessageBoxExA, MessageBoxExW;
  • MessageIndirectA, MessageBoxIndirectW;
  • MessageBoxTimeoutA, MessageBoxTimeoutW.

以A系列为例,它们的原型如下:

int MessageBoxA(
    IN HWND hWnd,
    IN LPCWSTR lpText,
    IN LPCWSTR lpCaption,
    IN UINT uType
    );
    
int MessageBoxExA(
    IN HWND hWnd,
    IN LPCWSTR lpText,
    IN LPCWSTR lpCaption,
    IN UINT uType,
    IN WORD uLanguageId
    );
    
int MessageBoxIndirectA(
    IN const MSGBOXPARAMSA *lpmbp
    );
    
int MessageBoxTimeoutA(
    IN HWND hWnd,
    IN LPCWSTR lpText,
    IN LPCWSTR lpCaption,
    IN UINT uType,
    IN WORD uLanguageId,
    IN DWORD dwMilliseconds
    );

// 其中MessageBoxIndirect的MSGBOXPARAMSA结构如下
typedef struct tagMSGBOXPARAMSA {
    UINT           cbSize;              // sizeof(MSGBOXPARAM)
    HWND           hwndOwner;          	// 对应其他函数的hWnd参数
    HINSTANCE      hInstance;          	// 含有图标的模块句柄(基址)
    LPCSTR         lpszText;            // 对应其他函数的lpText
    LPCSTR         lpszCaption;         // 对应其他函数的lpCaption
    DWORD          dwStyle;             // 对应其他函数的uType
    LPCSTR         lpszIcon;            // 消息框的图标
    DWORD_PTR      dwContextHelpId;     // 帮助ID(作为参数在回调函数中使用)
    MSGBOXCALLBACK lpfnMsgBoxCallback;  // 帮助按钮的回调函数
    DWORD          dwLanguageId;        // 对应其他函数的uLanguageId
} MSGBOXPARAMSA, *PMSGBOXPARAMSA, *LPMSGBOXPARAMSA;

还有ShellMessageBoxAShellMessageBoxW在shell32.dll中:

int WINCAPI ShellMessageBox(
    HINSTANCE hInst, 
    HWND hWnd, 
    LPCSTR pszMsg, 
    LPCSTR pszTitle, 
    UINT fuStyle, 
    ...
    );

其中某些通用的参数含义如下:

  • hWnd
    父窗口的句柄。可以为NULL.
  • lpText
    消息框的正文。
  • lpCaption
    消息框的标题。
  • uType
    消息框的样式,是一个Flag,有多个值可选。

关于按钮的uType:

Flag消息框的按钮
MB_OK0x0确定
MB_OKCANCEL0x1确定和取消
MB_ABORTRETRYIGNORE0x2终止、重试和忽略
MB_YESNOCANCEL0x3是、否和取消
MB_YESNO0x4是、否
MB_RETRYCANCEL0x5重试和取消
MB_CANCELTRYCONTINUE0x6取消、重试和继续
MB_HELP0x4000帮助(增加一个按钮)
MB_DEFBUTTON10x0(默认选中第一个按钮)
MB_DEFBUTTON20x100(默认选中第二个按钮)
MB_DEFBUTTON30x200(默认选中第三个按钮)

关于图标的uType:

Flag消息框的图标
MB_ICONSTOP0x10停止
MB_ICONQUESTION0x20询问
MB_ICONEXCLAMATION0x30警告
MB_ICONINFORMATION0x40消息

关于模态的uType:

Flag含义
MB_APPLMODAL0x0在用户对消息框做出回应之前,父窗口和它的其它子窗口将被禁用(disabled).
MB_SYSTEMMODAL0x1000效果同MB_APPMODAL,但是该消息框保持置顶。
MB_TASKMODAL0x2000如果指定了父窗口,效果同MB_APPMODAL;如果没指定,在用户对消息框做出回应之前,当前线程的全体顶层窗口将被禁用。

其他uType:

Flag含义
MB_SETFOREGROUND0x10000消息框窗口前置。
MB_DEFAULT_DESKTOP_ONLY0x20000如果当前桌面不是默认桌面,则在切换到默认桌面后才返回。
MB_TOPMOST0x40000消息框窗口保持置顶。
MB_RIGHT0x80000消息文本右对齐。
MB_SERVICE_NOTIFICATION0x200000即使没有用户登陆到桌面,也能弹出窗口。

它们的返回值含义如下:

返回值点击的按钮
IDOK1确定
IDCANCEL2取消
IDABORT3终止
IDRETRY4重试
IDIGNORE5忽略
IDYES6
IDNO7
IDTRYAGAIN10重试
IDCONTINUE11继续

带有的-A后缀表示此函数接收的字符串为ANSI编码,而-W表示接收Unicode字符串。

绝大多数接收字符串的API都有A和W系列的,除了少数像GetProcAddress以外。Windows的内核使用Unicode字符串,所以调用W系列的函数往往会更直截了当。微软推荐使用MultiByteToWideChar函数转换字符串编码,然而user32调用MBToWCS函数,该函数多数情况下直接调用更快的RtlMultiByteToUnicodeN函数。

几个API的调用关系

根据xp源代码,可以发现MessageBoxA只是简单调用了MessageBoxExA,并把第5个参数LanguageId设为0,而MessageBoxExA又直接调用了MessageBoxTimeoutA,并把第6个参数Timeout设为0。 MessageBoxTimeout是一个可以指定消息框显示时间的函数,到了指定的时间后对话框就自动消失,并返回默认按钮的值,可以利用这个特点来进行毫秒级的延时。 MessageBoxTimeoutA把字符串一转,便去调用MessageBoxTimeoutW。相反,如果直接调用MessageBoxW,那么就会一路很顺畅地调用到MessageBoxTimeoutW。此外,MessageBoxIndirectA也是靠MessageBoxIndirectW实现的。

ShellMessageBoxA
MessageBoxA
MessageBoxExA
MessageBoxTimeoutA
MessageBoxTimeoutW
ShellMessageBoxW
MessageBoxW
MessageBoxExW
MessageBoxWorker
MessageBoxIndirectA/W

到了vista以后,调用关系发生了变化,MessageBox直接调用MessageBoxTimeout,在Windows10 1909上,调用关系如下:

ShellMessageBoxA
MessageBoxA
MessageBoxTimeoutA
MessageBoxTimeoutW
ShellMessageBoxW
MessageBoxW
MessageBoxWorker
MessageBoxExA
MessageBoxExW
MessageBoxIndirectA/W

顺便提一个细节,xp源代码中MessageBoxIndirect需要检查MSGBOXPARAMS.cbSize是否为sizeof(MSGBOXPARAMS),但是到了win10 1909就没有检查的代码了

继续追踪下去,MessageBoxTimeoutWMessageBoxIndirect将有关的数据填入一个名为MSGBOXDATA的结构中,然后调用MessageBoxWorker函数。MessageBoxWorker函数位于user32.dll中,这个函数没有导出。

int MessageBoxWorker(LPMSGBOXDATA pMsgBoxParams);

typedef struct _MSGBOXDATA {        // size = 160
    MSGBOXPARAMS;                   // size = 80
    PWND     pwndOwner;             // 所有者窗口对象(似乎没用到)
    WORD     wLanguageId;           // 相当于其他函数的uLanguage参数
    INT    * pidButton;             // 按钮的ID
    LPWSTR * ppszButtonText;        // 各个按钮的文字
    UINT     cButtons;              // 按钮数量
    UINT     DefButton;             // 默认选中第几个按钮
    UINT     CancelId;              // 点击右上角的×号相当于点击哪个按钮
    /* win2k 新增 */
    DWORD    dwTimeout;             // 相当于MessageBoxTimeout的dwMilliseconds参数
    /* XP 新增 */
    HWND   * phwndList;             // 没用到
    /* vista 新增,以下的成员名称根据IDA猜测而来 */
    CHAR     unknown[16];           
    WORD     cxMsgFontChar;         // 单个系统字符的平均宽度(用处见下)
    WORD     cyMsgFontChar;         // 单个系统字符的平均高度(用处见下)
    CHAR     unknown2[2];           
} MSGBOXDATA, *PMSGBOXDATA, *LPMSGBOXDATA;

MessageBoxWorker之前的函数都只是简单地调用下一个函数,正剧从这里开始,参数将得到真正的处理。
这个函数首先非常快速地检查参数是否合法,如果消息框没有标题,就命为“错误”两个字。重点在dwStyle的MB_SERVICE_NOTIFICATION标志,这个标志存在与否将控制流走向分成了两条路,接下来分别详细讨论。

第一条路:一般的消息框(待完善)

在Win10 1909中,MessageBoxWorker首先根据dwStyle参数获取以下两个数据:

  • 按钮数量
    MsgBoxData.dwStyle的低4位索引一个名为mpTypeCcmd的全局数组。这个数组的名称是xp源代码中的,win10的符号文件并没有提供,以下出现的几个全局数组的名称也是这样。
  • 每个按钮上的文字
    首先以MsgBoxData.dwStyle的低4位索引一个名mpTypeIich的全局数组,再以此为索引访问SEBbuttons的全局数组,获取按钮文字的资源ID。如果LanguageId==0,那么以该资源ID作为索引直接从gpsi->MBString数组中获取,否则使用LoadStringBaseExW函数从user32中加载。
    user32、gdi32、win32k这些模块的开发者将缩写用到了极致,gpsi是Global Pointer of Server Information的缩写,存放着来自win32k的各种数据。

至于消息框的图标和弹框时播放的声音,这是接下来的事。

接着调用NtUserModifyUserStartupInfoFlags这个未导出函数,这个函数直接开始系统调用,进入内核查Shadow SSDT表以后调用win32k模块中的同名函数,修改进程的STARTUPINFO结构,将其dwFlags修改为STARTF_USESHOWWINDOW,防止有一些程序在初始化过程中遇到错误弹不出消息框。最后,MessageBoxWorker调用SoftModalMessageBox函数来弹出消息框:

int  SoftModalMessageBox(LPMSGBOXDATA lpmb);

SoftModalMessageBox是一个导出函数,它可以创建任意个数按钮、任意图标、可延时、任意按钮文字的消息框。按道理,这么强大又简洁的函数是不应该被导出的,但是有一个原因使它被迫导出,接下来会说明(为另一条大路埋下伏笔)。这个函数主要做以下几件事:

  • 计算消息框的尺寸
    消息框的尺寸需要考虑几个部分:

    • 按钮的个数
      按钮个数在MSGBOXDATA中给出,
    • 标题和正文的长度
    • 屏幕的宽度
    • 边框的厚度
    • 是否有图标

    补充:对话框基本单元(dialog base unit)和对话框模板单元(dialog template unit)
    对话框基本单元是一个方形区域(或者说是面积单位),和对话框所用字体有关,宽和高等于该字体单个字符宽和高的平均值(舍入到整数,以像素计)。对话框模板单元的宽度等于基本单元的1/4,高度等于基本单元的1/8. 消息框所使用的字体各种信息都存储在gpsi中,SoftModalMessageBox直接就拿来用了。而对于一般的应用程序,可使用GetDialogBaseUnits这个API获取系统对话框基本单元的大小(这个API直接使用gpsi->cxSysFontChargpsi->cySysFontChar)。

  • 确定消息框的图标

  • 针对dwStyle参数播放声音

  • 创建对话框模板

这个函数根据消息框的标题文字长度、正文的长度与行数对消息框窗口的长和宽进行计算,同时算清消息框的坐标(把消息框放到屏幕中心)。就像我们用SDK制作一个POPUP样式的窗口,这个函数也采用相同的套路:用NtUserGetDCEx(和GetDcEx是同一个函数,只不过user32.dll内部使用不同名称)获取DC,然后用DrawText绘制文字等。最后,它指定MB_DlgProcW为消息处理函数,调用InternalDialogBox(即DialogBoxParam的内部实现)来创建一个弹出式的窗口,其指定的资源在user32.dll初始化时已经准备好了。

顺便提一下,百度上说DialogBoxParam最终使用CreateWindowEx来创建窗口。其实,详细的过程为:DialogBoxParam使用FindResourceLoadResourceLockResource查找、加载并锁定资源,再利用资源的内存指针调用DialogBoxParamIndirectAorW->InternalDialogBoxInternalDialogBox调用InternalCreateDialog,在这其中才调用VerNtUserCreateWindowsEx->NtUserCreateWindowEx(起这么长的函数名打起来真是心累),此函数和CreateWindowEx还是有一点差别的。

第二条路:硬错误消息框

来说说第二条大路,即指定了MB_SERVICE_NOTIFICATION标志,这条路和操作系统内核就有比较密切的关系了。走了这条路,所弹出的Box会有非常神奇的效果:

  • 弹出一个消息窗口在当前的活动桌面上,即使没有用户登录到该桌面上
  • 消息窗口永远保持最置前的状态(Override Mode),置前的程度高于MB_TOPMOST,或者其他设置了HWND_TOPMOST标志的窗口
  • 在这个消息窗口关闭之前,不能再有其他设置了此标志的消息窗口弹出,如果尝试弹出,则在第一个窗口得到响应之后才会出现
  • 消息窗口的所属进程为CSRSS.exe,而不是调用MessageBox的进程

MessageBoxWorker简单地调用 ServiceMessageBox:

int ServiceMessageBox(
    IN LPCWSTR pText,
    IN LPCWSTR pCaption,
    IN UINT wType,
    IN DWORD dwTimeout
    );

很简单,就四个最基本的参数。ServiceMessageBox首先判断当前线程是否运行在与当前进程不同的 Session 上(即线程是否拥有其他 Session 的模拟令牌)。实现步骤是用NtOpenThreadTokenOpenThreadToken的实现)打开当前线程的令牌,再用NtQueryInformationTokenGetTokenInformation的实现)查询令牌的 Session ID ,与当前进程的 Session ID 比较。

当前进程的Session ID其实就在进程环境块PEB中,一行代码NtCurrentPeb()->SessionId即可搞定,但PEB结构没有文档化,所以还是调WTSGetActiveConsoleSessionId算了,这个函数也就相当于:USER_SHARED_DATA->ActiveConsoleId;

在NT5.0以前,不讨论跨session的情况。ServiceMessageBox函数简单填充HARDERROR_MSG结构,就调用CsrClientCallServer函数把请求通过LPC发到CSRSS去了。ntdll中有一个未导出的全局变量CsrPortHandle,是CSR LPC端口的句柄,CsrClientCallServer用的就是这个句柄。

如果Session ID不相同

如果Session ID不相同,则调用winsta.dll中的WinStationSendMessage(当然要导出啦,不然怎么调用,它同时也是WTSSendMessage的实现)向当前线程模拟令牌指定的Session弹出消息窗口。这个函数使用了RPC通知CSRSS,效率比较低,所以才进行之前的Session判断。

BOOLEAN
WINAPI
WinStationSendMessageW(
    IN HANDLE hServer,      //在win10中是一个CSmartBinding的类指针,不理他,填0
    IN ULONG SessionId,     //模拟令牌的Session ID
    IN PWSTR Title,         //消息窗口标题
    IN ULONG TitleLength,   //标题长度(以字节计)
    IN PWSTR Message,       //消息正文
    IN ULONG MessageLength, //正文长度(以字节计)
    IN ULONG Style,         //窗口样式,同之前的dwStyle参数
    IN ULONG Timeout,       //消息框自动消失时间(以秒计)
    OUT PULONG Response,    //消息返回值,同MessageBox的返回值
    IN BOOLEAN DoNotWait    //是否等待消息窗口返回,填TRUE的话Response的结果就是未定义的
    );

这个函数我们可以自己调着玩,示例代码如下。注意,编译时别忘了包含静态库,对于gcc,需要包含libwinsta.a,对于msvc,需要包含winsta.lib. 当然也可以不包含,只要用GetModuleHandle获取winsta.dll的模块地址,然后用GetProcAddress获取函数地址即可。

PWSTR text = L"text";
PWSTR caption = L"caption";
ULONG ret;

WinStationSendMessageW(
    NULL,                             // 填NULL
    WTSGetActiveConsoleSessionId(),   // 当前进程的会话ID
    caption,                          // 消息框标题
    wcslen(caption) * sizeof(WCHAR),  // 标题字符串长度(以字节计)
    text,                             // 消息框正文
    wcslen(text) * sizeof(WCHAR),     // 正文字符串长度(以字节计)
    MB_ICONWARNING,                   // 同上文的dwStyle
    0,                                // 自动消失时间(以秒计),不想自动消失就填0或-1
    &ret,                             // 消息返回值,同MessageBox的返回值
    FALSE                             // 是否等待消息窗口返回
    );

WinStationSendMessage也有A和W两个版本,这个时候字符串早就已经是unicode了,所以只用到W版本。这个函数内部

如果Session ID相同

如果Session ID一致,则调用系统服务NtRaiseHardError,这是个很好用的系统服务,原型和使用方法如下:

//原型
NTSYSAPI
NTSTATUS
NTAPI
NtRaiseHardError(
    IN NTSTATUS ErrorStatus,
    IN ULONG NumberOfParameters,
    IN ULONG UnicodeStringParameterMask,
    IN PULONG_PTR Parameters,
    IN ULONG ValidResponseOptions,
    OUT PULONG Response //对应HARDERROR_RESPONSE
    );

// 其中*Response的可能值如下
typedef enum _HARDERROR_RESPONSE {
    ResponseReturnToCaller,
    ResponseNotHandled,
    ResponseAbort,     //意思同IDABORT
    ResponseCancel,    //IDCANCEL
    ResponseIgnore,    //IDIGNORE
    ResponseNo,        //IDNO
    ResponseOk,        //IDOK
    ResponseRetry,     //IDRETRY
    ResponseYes,       //IDYES
    ResponseTryAgain,  //IDTRYAGAIN
    ResponseContinue   //IDCONTINUE
} HARDERROR_RESPONSE;

这个函数我们也可以自己调着玩,示例代码如下。注意,编译时别忘了包含静态库,对于gcc,需要包含libntdll.a,对于msvc,需要包含ntdll.lib或ntdllp.lib. 当然也可以不包含,只要用GetModuleHandle获取ntdll.dll的模块地址,然后用GetProcAddress获取函数地址即可。
另外这段代码在内核中也可以用,前提是将NtRaiseHardError改成ExRaiseHardError,两个函数的参数完全相同。

//RtlInitUnicodeString MSDN有相关文档
#define STATUS_SERVICE_NOTIFICATION 0x40000018L
#define HARDERROR_OVERRIDE_ERRORMODE 0x10000000L

ULONG_PTR Parameters[4];
PWSTR pText = L"消息正文";
PWSTR pCaption = L"标题";
ULONG Response;
UNICODE_STRING Text, Caption;

RtlInitUnicodeString(&Text, pText);
RtlInitUnicodeString(&Caption, pCaption);
Parameters[0] = (ULONG_PTR)&Text;
Parameters[1] = (ULONG_PTR)&Caption;
Parameters[2] = MB_YESNO;           //同MessageBox的uType
Parameters[3] = 0;                  //同MessageBoxTimeout的Timeout,单位为毫秒

NtRaiseHardError(
    STATUS_SERVICE_NOTIFICATION | HARDERROR_OVERRIDE_ERRORMODE,
    4,
    3,
    Parameters,
    OptionOk,
    &Response
    );

NtRaiseHardError进入内核后调用内核模块中的同名函数NtRaiseHardError->ExpRaiseHardError。后者的处理根据操作系统版本的不同而不同。

  • Windows 2000 (NT 5.0) 至 Windows Server 2003 (NT 5.1)
    ExpRaiseHardError调用 LPC函数LpcRequestWaitReplyPortEx通知CSRSS,并传入HARDERROR_MSG结构,这个结构从xp到win11都没有改变过。目标LPC端口是本进程的EPROCESS.ExceptionPort
  • Windows Vista (NT 6.0) 以后
    Vista的内核是一次史诗级更新,整个LPC的代码都删光了,换上了新的ALPC。为了保持兼容性,内核导出的LPC API依然存在,但它们仅简单地调用ALPC的相关函数。ExpRaiseHardError调用的是LpcSendWaitReceivePort函数,这个函数只是进入临界区之后调用ALPC组件的AlpcpProcessSynchronousRequest函数而已。目标端口保持不变。

上文提到的HARDERROR_MSG结构如下:

typedef struct _HARDERROR_MSG {
    PORT_MESSAGE h;                   //LPC端口消息的必要头部
    NTSTATUS Status;                  //STATUS_SERVICE_NOTIFICATION
    LARGE_INTEGER ErrorTime;          //当前时间,由ExpRaiseHardError调用KeGetCurrentTime()产生
    ULONG ValidResponseOptions;       //同NtRaiseHardError的同名参数
    ULONG Response;                   //同NtRaiseHardError的同名参数
    ULONG NumberOfParameters;         //同NtRaiseHardError的同名参数(=4)
    ULONG UnicodeStringParameterMask; //同NtRaiseHardError的同名参数(=3)
    ULONG_PTR Parameters[5];          //同NtRaiseHardError的同名参数
} HARDERROR_MSG, *PHARDERROR_MSG;

我们可以更进一步地,在内核中通过LPC通知CSRSS,示例代码如下,该程序在win10 1909上成功运行。

    // 获取当前进程session id
    token = PsReferencePrimaryToken(IoGetCurrentProcess());
    Status = SeQuerySessionIdToken(token, &sessionId);
    PsDereferencePrimaryToken(token);
    if (!NT_SUCCESS(Status)) return Status;
    
    // 注意,这里的Text和Caption是宽字符串指针,而这两个宽字符串必须要在用户空间中
    // 否则CSRSS无法处理这些字符串
    RtlInitUnicodeString(&UText, Text);
    RtlInitUnicodeString(&UCaption, Caption);
    swprintf_s(portName, 250, L"\\Sessions\\%d\\Windows\\ApiPort", sessionId);
    RtlInitUnicodeString(&UPort, portName);
    
    // ObReferenceObjectByName没有文档化,去WRK中找定义吧
    Status = ObReferenceObjectByName(
        &UPort, OBJ_CASE_INSENSITIVE, NULL, 0, 
        *LpcPortObjectType, KernelMode, NULL, &Port
        );
    if(!NT_SUCCESS(Status)) return Status;

    __try {
        m->h.u1.Length = 
            sizeof(HARDERROR_MSG) << 16 | (sizeof(HARDERROR_MSG) - sizeof(PORT_MESSAGE));
        m->h.u2.ZeroInit = LPC_ERROR_EVENT;  // LPC_ERROR_EVENT = 9
        m->Status = STATUS_SERVICE_NOTIFICATION; // STATUS_SERVICE_NOTIFICATION = 0x40000018
        m->ValidResponseOptions = 0;
        m->UnicodeStringParameterMask = 3;
        m->NumberOfParameters = 4;
        m->Response = 0;
        m->Parameters[0] = (ULONG_PTR)UText;
        m->Parameters[1] = (ULONG_PTR)UCaption;
        m->Parameters[2] = (ULONG_PTR)Style;
        m->Parameters[3] = (ULONG_PTR)Timeout;
        m->Parameters[4] = 0;
        KeQuerySystemTime(&m->ErrorTime);
    }__except(EXCEPTION_EXECUTE_HANDLER) {
        ObDereferenceObject(Port);
        return GetExceptionCode();
    }

    Status = LpcRequestWaitReplyPortEx(Port, (PPORT_MESSAGE)m, (PPORT_MESSAGE)m);
    ObDereferenceObject(Port);
    return Status;

总之,走这条大路都离不开CSRSS,那为什么要进入到内核这么麻烦呢?
因为指定了MB_SERVICE_NOTIFICATION的消息窗口主要被用于服务端对客户端的通知,在SCM看来,内核中的驱动模块也是一种服务,所以,内核中也提供了相应的函数来实现这个过程,如IoRaiseHardErrorIoRaiseInformationalHardError,他们最终都是调用ExRaiseHardError -> ExpRaiseHardError来实现的。

到这里,我们可以总结一下ServiceMessageBox之后、请求到达CSRSS之前的函数调用情况

会话ID相同
syscall
会话ID不同
RPC转LPC
syscall
ServiceMessageBox
NtRaiseHardError
ExpRaiseHardError
LpcSendWaitReceivePort
AlpcpProcessSynchronousRequest
WinStationSendMessageW
CSmartSession::ShowMessageBox
NtAlpcSendWaitReceivePort

CSRSS做了什么

那么,通知CSRSS后,它做了些什么呢?

在NT4上,CSRSS的其中一个LPC端口服务线程接收到LPC_ERROR_EVENT消息后,就去调用LoadedServerDll->HardErrorRoutine,这个例程在CSR初始化时就已经设置好的了。这个例程到底在哪?网络上搜不到任何有关资料。翻翻NT4源代码,再结合IDA和调试器,发现这个例程是winsrv.dll中的一个未导出函数UserHardError->UserHardErrorEx,并传入HARDERROR_MSG结构和CSRSS自己储存的关于引起错误的线程信息,原型如下:

VOID UserHardError(
    PCSR_THREAD pt,
    PHARDERROR_MSG pmsg
    );

VOID UserHardErrorEx(
    PCSR_THREAD pt,
    PHARDERROR_MSG pmsg,
    PCTXHARDERRORINFO pCtxHEInfo
    );

UserHardError经过一轮参数检查,从发起消息窗口的进程中复制消息正文和标题,若 HARDERRORMSG.ValidResponseOptions == OptionOkNoWait(对应NtRaiseHardError的同名参数和WinStationSendMessage的DoNotWait),则立马通知CSRSS返回,并创建一个新线程来弹出窗口。创建新线程在ProcessHardErrorRequest中实现,这个函数还会调用HardErrorHandler()(零参数),做最后的实现。

高潮来了,HardErrorHandle调用NtUserHardErrorControl,对当前线程做一些奇奇怪怪的事情[ Win32k全局变量重设置(由此决定消息窗口的唯一性)、切换桌面(由此决定消息窗口的前置性)、加入消息队列(由此决定下一个类似消息窗口的可用性)等等],确保当前线程有能力弹出MB_SERVICE_NOTIFICATION样式的窗口。

新线程
CsrApiRequestThread
QueueHardError
UserHardErrorEx
ProcessHardErrorRequest
HardErrorHandler
MessageBoxTimeoutW
TerminalServerRequestThread
W32WinStationDoMessage
RemoteMessageThread
UINT NtUserHardErrorControl(
    IN HARDERRORCONTROL dwCmd,
    IN HANDLE handle,
    OUT PDESKRESTOREDATA pdrdRestore OPTIONAL
    );

设置完之后,HardErrorHandler调用SoftModalMessageBox(所以这个函数必须要导出),弹出消息窗口,回到了第一条大路。但由于 NtUserHardErrorControl的功劳,这个窗口变得唯一、最前置、不美观(Windows7之后修复了这个问题)。

所以,一个简简单单的MessageBox,却要牵涉到模态、窗口、消息、RPC/LPC、桌面、会话、系统服务等机制,需要user32.dll,ntdll,内核,winsta.dll,csrss.exe,winsrv.dll,win32k.sys等模块的参与。这似乎印证了一个道理:Windows中,使用越方便的API,背后的原理越复杂。


版权声明:本文为AcceZn原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。