Blog Post

Patching Bugs - Windows Update Service - Part 2

Second custom patch for the DLL hijack bug in the Windows Update Service.

Patching Bugs - Windows Update Service - Part 2 - Second custom patch for the DLL hijack bug in the Windows Update Service.
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:

C++[Copy]
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.)

C++[Copy]
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).

C++[Copy]
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:
    \Microsoft\Windows\UpdateOrchestrator\Reboot_AC
    with the command:
    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.)

C++[Copy]
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 the SE_SHUTDOWN_PRIVILEGE, which is somewhat risky for us, as there will be a whole 2-minute delay before the calling function invokes InitiateShutdownW after we return from our Shell_RequestShutdown override, and some other service may reinstate the SE_SHUTDOWN_PRIVILEGE in the meantime. Remember, the USO service will be running in the shared svchost.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:

C++[Copy]
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 invoke InitiateShutdownW if we return from our Shell_RequestShutdown override. Remember that Microsoft used the same code-base for the usosvc.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 (or usosvc.dll) in which case we cannot kill the calling process.

This will display a message like so:

Your device will restart to update outside of active hours
Toast notification - "Your device will restart to update outside of active hours".

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.)

C++[Copy]
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:

C++[Copy]
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:

Related Articles