This article contains functions and features that are not documented by the original manufacturer. By following advice in this article, you're doing so at your own risk. The methods presented in this article may rely on internal implementation and may not work in the future.
Intro
This is a third blog post dedicated to the vulnerability that we discovered in the Windows 10 update service. After our initial research, Dennis A. Babkin wrote his own small software patch that he described in the second blog post on this subject. But, if you remember, besides just patching the aforementioned vulnerability our goal has also become to find a way to let users indefinitely delay forced restarts after installation of updates on all versions of Windows 10.
After the first patch, Rbmm proposed an alternative solution that was somewhat simpler. This third blog post will be dedicated to his approach.
Overview
If you remember,
the bug consisted of the DLL hijack vulnerability in the Windows update service (usosvc.dll
) that was attempting to load
a non-existent module ShellChromeAPI.dll
and invoke the Shell_RequestShutdown
function, that was exported from it, before attempting to reboot the system.
So to notify the user and to delay a restart, Dennis A. Babkin in his interpretation simply
showed a message box in the intercepted Shell_RequestShutdown
function.
Catching Manual Requests For A Restart
Another option though was to notice that when a user manually requested a restart from the UI (be it from the Settings window, or from the
toast notification) that the operating system sets the following DWORD
Registry value to 1:
\REGISTRY\MACHINE\SOFTWARE\Microsoft\WindowsUpdate\UX\StateVariables
RebootUXLaunched REG_DWORD 0 / 1
Internally, this is done by calling the UxUsoShim::SetUxStateVariableBOOL
function.
Thus, in our override of the Shell_RequestShutdown
function we can use that Registry value to decide what to do. In other words, if we read it and it is not 0,
this means that the user manually requested a reboot:
bool IsRebootUXLaunched = false;
static const WCHAR kRk[] = L"\\REGISTRY\\MACHINE\\SOFTWARE\\Microsoft\\WindowsUpdate\\UX\\StateVariables";
static const UNICODE_STRING sRk = { sizeof(kRk) - sizeof((kRk)[0]), sizeof(kRk), const_cast<PWSTR>(kRk) };
static OBJECT_ATTRIBUTES oa = { sizeof(oa), 0, const_cast<PUNICODE_STRING>(&sRk), OBJ_CASE_INSENSITIVE };
HANDLE hKey;
if (0 <= ZwOpenKey(&hKey, KEY_QUERY_VALUE, &oa))
{
static const WCHAR kRbt[] = L"RebootUXLaunched";
static const UNICODE_STRING RebootUXLaunched = { sizeof(kRbt) - sizeof((kRbt)[0]), sizeof(kRbt), const_cast<PWSTR>(kRbt) };
KEY_VALUE_PARTIAL_INFORMATION kvpi;
IsRebootUXLaunched = 0 <= ZwQueryValueKey(hKey, const_cast<PUNICODE_STRING>(&RebootUXLaunched),
KeyValuePartialInformation, &kvpi, sizeof(kvpi), &kvpi.TitleIndex) &&
kvpi.DataLength == sizeof(ULONG) && kvpi.Type == REG_DWORD && *(ULONG*)kvpi.Data;
NtClose(hKey);
}
One caveat in this case though is this: if we determine that the user indeed wanted to restart the system to install updates, we need to reboot it ourselves.
(We need this because of the 2-minute delay
that Microsoft has in their code after Shell_RequestShutdown
returns.)
if (IsRebootUXLaunched)
{
ULONG dwShutdownFlags = SHUTDOWN_ARSO | SHUTDOWN_RESTARTAPPS | SHUTDOWN_RESTART |
SHUTDOWN_INSTALL_UPDATES | SHUTDOWN_FORCE_SELF | SHUTDOWN_FORCE_OTHERS;
if (GetShellWindow())
{
ExecUX(0, L"* ClearActiveNotifications", TRUE);
dwShutdownFlags = wcsstr(GetCommandLineW(), L"RebootWithUXForceOthers")
? SHUTDOWN_ARSO | SHUTDOWN_RESTARTAPPS | SHUTDOWN_RESTART | SHUTDOWN_INSTALL_UPDATES | SHUTDOWN_FORCE_OTHERS
: SHUTDOWN_ARSO | SHUTDOWN_RESTARTAPPS | SHUTDOWN_RESTART | SHUTDOWN_INSTALL_UPDATES;
}
InitiateShutdownW(0, 0, 0, dwShutdownFlags,
SHTDN_REASON_FLAG_PLANNED |
SHTDN_REASON_MAJOR_OPERATINGSYSTEM |
SHTDN_REASON_MINOR_SERVICEPACK);
return;
}
In the code above, we also need to clear toast notifications before we call InitiateShutdownW
.
We can do so by invoking MusNotificationUx.exe
with the ClearActiveNotifications
command line parameter.
But before we do that, we also need to make sure that we're running in an interactive desktop. We can achieve that by calling
GetShellWindow
that will return NULL if we are not.
I use the following helper function to communicate with MusNotificationUx.exe
- that is the UI process of the Windows 10 Update Service Orchestrator (USO).
void ExecUX(HANDLE hToken, PCWSTR CommandLine, BOOL bWait = FALSE)
{
WCHAR ApplicationName[MAX_PATH];
if (ExpandEnvironmentStringsW(L"%systemroot%\\system32\\MusNotificationUx.exe", ApplicationName, _countof(ApplicationName)))
{
PROCESS_INFORMATION pi;
STARTUPINFO si = { sizeof(si) };
if (CreateProcessAsUserW(hToken, ApplicationName, const_cast<PWSTR>(CommandLine), 0, 0, 0, 0, 0, 0, &si, &pi))
{
NtClose(pi.hThread);
if (bWait) WaitForSingleObject(pi.hProcess, INFINITE);
NtClose(pi.hProcess);
}
}
}
The code above starts an instance of the MusNotificationUx.exe
process with CommandLine
parameters, in the user session specified in the user token we pass in hToken
.
In actuality, the way USO performs a forced reboot is this:
- It sets a new Task Scheduler task, named:
with the command:
\Microsoft\Windows\UpdateOrchestrator\Reboot_AC
MusNotification.exe /RunOnAC Reboot
- And when the task fires, it runs:
MusNotificationUx.exe RebootWithUX
So in a sense, that is an example of how those two USO modules, MusNotification.exe
and MusNotificationUx.exe
, exchange commands between each other using the Task Scheduler.
Showing User Notification & Blocking Restart
So going back to our override of the Shell_RequestShutdown
function, if we detect that the user did not manually request a restart,
we need to display a UI that will prompt the user to do so.
But first we need to set the token of the thread that does not have the SE_SHUTDOWN_PRIVILEGE
privilege.
(This is needed to make the caller's InitiateShutdownW
function fail after
we return from the Shell_RequestShutdown
function
in our override.)
HANDLE hToken, hNewToken = 0;
if (0 <= NtOpenProcessToken(NtCurrentProcess(), TOKEN_DUPLICATE, &hToken))
{
BOOL b = DuplicateTokenEx(hToken, TOKEN_ADJUST_PRIVILEGES | TOKEN_IMPERSONATE,
0, ::SecurityImpersonation, ::TokenImpersonation, &hNewToken);
NtClose(hToken);
if (b)
{
if (0 <= NtAdjustPrivilegesToken(hNewToken, FALSE, const_cast<PTOKEN_PRIVILEGES>(&tp_No_Shutdown), 0, 0, 0))
{
NtSetInformationThread(NtCurrentThread(), ThreadImpersonationToken, &hNewToken, sizeof(hNewToken));
}
NtClose(hNewToken);
}
}
Unfortunately USO uses the process token to set theSE_SHUTDOWN_PRIVILEGE
, which is somewhat risky for us, as there will be a whole 2-minute delay before the calling function invokesInitiateShutdownW
after we return from ourShell_RequestShutdown
override, and some other service may reinstate theSE_SHUTDOWN_PRIVILEGE
in the meantime. Remember, the USO service will be running in the sharedsvchost.exe
process, among some other services.
After that we need to show a toast notification to the user and let them choose what to do:
static const WCHAR Toast_FairWarning[] = L"* Toast_FairWarningDesktop";
if (GetShellWindow())
{
ExecUX(0, Toast_FairWarning);
ExitProcess(0);
}
Note that I also check for an interactive desktop with a call to GetShellWindow
. And only if we're running in one, I display the toast notification
with the following command, and kill self with a call to
ExitProcess
:
MusNotificationUx.exe Toast_FairWarningDesktop
We need to kill the calling UI process to ensure that it doesn't invokeInitiateShutdownW
if we return from ourShell_RequestShutdown
override. Remember that Microsoft used the same code-base for theusosvc.dll
, as well as for other USO UI processes. The reason we check if we're running in an interactive desktop is to differentiate it from when our override is called from the system service (orusosvc.dll
) in which case we cannot kill the calling process.
This will display a message like so:
The good thing for us is that USO has a built-in mechanism for preventing multiple toast notifications from overlaying each other. In other words, if one such notification was already shown, it will be replaced with a new one. This way we should not be concerned with multiple notifications, as we would be in case we were just showing our own MessageBox.
One final condition to cover here is the situation when our Shell_RequestShutdown
function was not called from an interactive desktop.
This happens when the call to Shell_RequestShutdown
function comes from the USO service itself (or usosvc.dll
.)
ULONG dwSessionId = WTSGetActiveConsoleSessionId();
if (dwSessionId != MAXULONGLONG && WTSQueryUserToken(dwSessionId, &hToken))
{
ExecUX(hToken, Toast_FairWarning);
NtClose(hToken);
}
In that case, we retrieve the currently active user session ID and acquire its user token with a call to
WTSQueryUserToken
,
and then use it to show the toast notification in that user session.
Additional Actions
After our toast notification is shown, the further actions depend on the user's choice. If the user clicks on our toast notification the USO calls:
MusNotificationUx.exe -Embedding
Which then invokes the internal function InteractiveToastActivationHandler::Activate
through several
IPC calls
(which is the implementation of
INotificationActivationCallback::Activate
.)
The Activate
method for the Toast_FairWarningDesktop
command will be invoked with the following parameters:
/ToastAction ImmRestartNow /MusUxStateString 79 /ToastLaunchTimestamp ...
And then Activate
will call SetUxStateVariableBOOL(RebootUXLaunched, true)
, that in turn will call InvokeRestartNow
-> RebootToCompleteInstall
from usosvc
.
And then internally RebootToCompleteInstall
will invoke MusNotificationUx.exe
for the third time, but this time with a different parameter:
RebootWithUX
, or RebootWithUXForceOthers
(if there's more than one user logged on at that time).
Then it will call our Shell_RequestShutdown
with the RebootUXLaunched
set to 1. And in this case our override will call InitiateShutdownW
to reboot the system.
Proof-of-Concept Code
The resulting code can be compiled from the provided Visual Studio solution:
void ExecUX(HANDLE hToken, PCWSTR CommandLine, BOOL bWait = FALSE)
{
WCHAR ApplicationName[MAX_PATH];
if (ExpandEnvironmentStringsW(L"%systemroot%\\system32\\MusNotificationUx.exe", ApplicationName, _countof(ApplicationName)))
{
PROCESS_INFORMATION pi;
STARTUPINFO si = { sizeof(si) };
if (CreateProcessAsUserW(hToken, ApplicationName, const_cast<PWSTR>(CommandLine), 0, 0, 0, 0, 0, 0, &si, &pi))
{
NtClose(pi.hThread);
if (bWait) WaitForSingleObject(pi.hProcess, INFINITE);
NtClose(pi.hProcess);
}
}
}
void Shell_RequestShutdown(int)
{
bool IsRebootUXLaunched = false;
static const WCHAR kRk[] = L"\\REGISTRY\\MACHINE\\SOFTWARE\\Microsoft\\WindowsUpdate\\UX\\StateVariables";
static const UNICODE_STRING sRk = { sizeof(kRk) - sizeof((kRk)[0]), sizeof(kRk), const_cast<PWSTR>(kRk) };
static OBJECT_ATTRIBUTES oa = { sizeof(oa), 0, const_cast<PUNICODE_STRING>(&sRk), OBJ_CASE_INSENSITIVE };
HANDLE hKey;
if (0 <= ZwOpenKey(&hKey, KEY_QUERY_VALUE, &oa))
{
static const WCHAR kRbt[] = L"RebootUXLaunched";
static const UNICODE_STRING RebootUXLaunched = { sizeof(kRbt) - sizeof((kRbt)[0]), sizeof(kRbt), const_cast<PWSTR>(kRbt) };
KEY_VALUE_PARTIAL_INFORMATION kvpi;
IsRebootUXLaunched = 0 <= ZwQueryValueKey(hKey, const_cast<PUNICODE_STRING>(&RebootUXLaunched),
KeyValuePartialInformation, &kvpi, sizeof(kvpi), &kvpi.TitleIndex) &&
kvpi.DataLength == sizeof(ULONG) && kvpi.Type == REG_DWORD && *(ULONG*)kvpi.Data;
NtClose(hKey);
}
if (IsRebootUXLaunched)
{
ULONG dwShutdownFlags = SHUTDOWN_ARSO | SHUTDOWN_RESTARTAPPS | SHUTDOWN_RESTART |
SHUTDOWN_INSTALL_UPDATES | SHUTDOWN_FORCE_SELF | SHUTDOWN_FORCE_OTHERS;
if (GetShellWindow())
{
ExecUX(0, L"* ClearActiveNotifications", TRUE);
dwShutdownFlags = wcsstr(GetCommandLineW(), L"RebootWithUXForceOthers")
? SHUTDOWN_ARSO | SHUTDOWN_RESTARTAPPS | SHUTDOWN_RESTART | SHUTDOWN_INSTALL_UPDATES | SHUTDOWN_FORCE_OTHERS
: SHUTDOWN_ARSO | SHUTDOWN_RESTARTAPPS | SHUTDOWN_RESTART | SHUTDOWN_INSTALL_UPDATES;
}
InitiateShutdownW(0, 0, 0, dwShutdownFlags,
SHTDN_REASON_FLAG_PLANNED |
SHTDN_REASON_MAJOR_OPERATINGSYSTEM |
SHTDN_REASON_MINOR_SERVICEPACK);
return;
}
static const TOKEN_PRIVILEGES tp_No_Shutdown = { 1, { { { SE_SHUTDOWN_PRIVILEGE } } } };
HANDLE hToken, hNewToken = 0;
if (0 <= NtOpenProcessToken(NtCurrentProcess(), TOKEN_DUPLICATE, &hToken))
{
BOOL b = DuplicateTokenEx(hToken, TOKEN_ADJUST_PRIVILEGES | TOKEN_IMPERSONATE,
0, ::SecurityImpersonation, ::TokenImpersonation, &hNewToken);
NtClose(hToken);
if (b)
{
if (0 <= NtAdjustPrivilegesToken(hNewToken, FALSE, const_cast<PTOKEN_PRIVILEGES>(&tp_No_Shutdown), 0, 0, 0))
{
NtSetInformationThread(NtCurrentThread(), ThreadImpersonationToken, &hNewToken, sizeof(hNewToken));
}
NtClose(hNewToken);
}
}
static const WCHAR Toast_FairWarning[] = L"* Toast_FairWarningDesktop";
if (GetShellWindow())
{
ExecUX(0, Toast_FairWarning);
ExitProcess(0);
}
ULONG dwSessionId = WTSGetActiveConsoleSessionId();
if (dwSessionId != MAXULONGLONG && WTSQueryUserToken(dwSessionId, &hToken))
{
ExecUX(hToken, Toast_FairWarning);
NtClose(hToken);
}
}
To install, make sure to compile a 32-bit or 64-bit Release
configuration (depending on the bitness of the target operating system) and place the resulting
ShellChromeAPI.dll
file info the system folder:
%WinDir%\System32
To uninstall, simply remove the ShellChromeAPI.dll
file from the system folder that I showed above.
Conclusion
Finally, if you are interested:
- You can download the full source code for the project described in this blog post as the Visual Studio 2019 solution.