Home Showing a MessageBox from kernel-mode
Post
Cancel

Showing a MessageBox from kernel-mode

Introduction

Message boxes provide a simple way to show feedback to the user. In user-mode, a message box can be shown with the MessageBoxW API function. However, this API does not exist in kernel-mode. One of the common ways to display info from kernel-mode is the DbgPrint API, which can be listened to from DbgView or an attached Kernel debugger. Recently I wanted to display a message box from kernel-mode though, so the user can receive feedback easily. There already are sources on how it’s done out on the web, but I want to take you through the inner workings of the MessageBox function itself and add more information to it.

This entire procedure only works if the calling thread belongs to a user-mode process. This means it works when manually mapping a driver with a mapper like kdmapper. The reason for this limitation will be explained later.

MessageBoxW

The MessageBoxW API is located inside of user32.dll. The only thing it does is to call MessageBoxTimeoutW. Inside of MessageBoxTimeoutW, a struct is initialized which holds all arguments for the message box, such as the text, caption, and type. Finally, MessageBoxWorker gets called. This function runs some extra checks, for example, if the current thread is a GUI thread. It also checks whether a parent window handle was passed into MessageBoxW. If that is the case, the function calls into SoftModalMessageBox, otherwise, it calls ServiceMessageBox. Since we don’t have a parent window handle in kernel-mode, we are going to enter ServiceMessageBox.
This is where interesting things happen. After checking the threads and processes session IDs it puts the message box arguments into an array and calls the NtRaiseHardError API from ntdll.dll. The ErrorStatus argument is set to 0x50000018, which is STATUS_SERVICE_NOTIFICATION.

1
2
3
4
5
6
7
8
RtlInitUnicodeString(&lpTextStr, lpText);
RtlInitUnicodeString(&lpCaptionStr, lpCaption);
arguments[2] = MsgBoxType2;
arguments[0] = &lpTextStr;
arguments[3] = v4;
arguments[1] = &lpCaptionStr;
if ( NtRaiseHardError(0x50000018, 4, 3, arguments, 1, &response) < 0 || (v20 = response, response >= 0xB) )
    v20 = 1i64;

Implementation

We can reproduce this behavior easily. The kernel-mode API for NtRaiseHardError is the ntoskrnl export ExRaiseHardError. The Response argument even returns the message boxes response, for example, which button the user clicked on a Yes/No prompt. The types match the one from user-mode (e.g. MB_OK, MB_YESNO).

As I mentioned above, the calling thread must belong to a user-mode thread. If you use the Service Manager to start your driver and call this in your DriverEntry it will not work. However, if you call it in your IOCTL handler it is going to work because the thread originated from a DeviceIoControl call in a user-mode process. The reason for this limitation is, that ExRaiseHardError in kernel-mode eventually does a Local Procedure Call to the exception port of the executing process. The System process does not have that. Furthermore, the first bit in PEPROCESS->DefaultHardErrorProcessing has to be set accordingly. I have tried attaching to another process using KeStackAttachProcess, this caused a MessageBox to show up, but it was empty and at this point, I stopped going further.

For the people manually mapping their drivers however and wanting simple feedback for a potential user, here is the implementation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
extern "C" NTSTATUS NTAPI ExRaiseHardError(
    NTSTATUS ErrorStatus, ULONG NumberOfParameters, 
    ULONG UnicodeStringParameterMask, PULONG_PTR Parameters,
    ULONG ValidResponseOptions, PULONG Response);

ULONG KeMessageBox(PCWSTR title, PCWSTR text, ULONG_PTR type)
{
    UNICODE_STRING uTitle = { 0 };
    UNICODE_STRING uText = { 0 };

    RtlInitUnicodeString(&uTitle, title);
    RtlInitUnicodeString(&uText, text);

    ULONG_PTR args[] = { (ULONG_PTR)&uText, (ULONG_PTR)&uTitle, type };
    ULONG response = 0;

    ExRaiseHardError(STATUS_SERVICE_NOTIFICATION, 3, 3, args, 2, &response);
    return response;
}

Usage example:

1
2
#define MB_OK 0x00000000L
ULONG result = KeMessageBox(L"Hello world!", L"Greetings from kernel-mode", MB_OK);

This is the enum containing all possible results:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef enum _HARDERROR_RESPONSE
{
    ResponseReturnToCaller,
    ResponseNotHandled,
    ResponseAbort,
    ResponseCancel,
    ResponseIgnore,
    ResponseNo,
    ResponseOk,
    ResponseRetry,
    ResponseYes,
    ResponseTryAgain,
    ResponseContinue
} HARDERROR_RESPONSE;
This post is licensed under CC BY 4.0 by the author.