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
Some time ago, Rbmm and I wrote a blog post after we discovered a vulnerability in the Windows 10 update service. The bug consisted of a trivial DLL hijack of a missing system module. You can read all the details about it here.
As before, we submitted our findings to the Microsoft Security Response Center (MSRC) and, as it has become almost customary now, received the following run-around:
Hello Dennis,
Thank you again for submitting this issue to Microsoft. We determined that a fix will not be released for the reported behavior as UAC is not considered a security boundary , and the dll loading would fall into the "Path directories DLL planting" category as described here
https://msrc-blog.microsoft.com/2018/04/04/triaging-a-dll-planting-vulnerability/
We have therefore closed this case.
If you have any questions, or additional information related to this report, please reply on this case thread.
Thank you very much for working with us.
Regards,
**Name-Redacted**
MSRC
Note that UAC had nothing to do with the bug ... but oh, well. I'm not gonna raise my blood pressure.
At first we decided not to worry about it anymore, since the original manufacturer had refused to fix their bug, and clearly cared less.
But after a short while, Rbmm had a brilliant idea, which lead me to write the software that I will explain in this blog post. After all, I was using Windows 10 myself, and I didn't want to leave my computer exposed to this vulnerability.
Quick Recap
Let's quickly recap the bug. (Again for the full details read this blog post.)
At some point in the past, Microsoft evidently had a DLL, called ShellChromeAPI.dll
that was used for the Windows Phone platform.
As soon as that platform became no-more, they removed that DLL, but did not remove any code that tried to load it.
Rbmm and I had discovered the following internal function that was called right before initiating a restart from the Windows update service (USOsvc.dll
),
as well as from the UI process (MusNotificationUx.exe
) that pesters users with the update notifications on the screen:
NTSTATUS ShellReboot(void)
{
HMODULE hModule;
FARPROC pFunc;
NTSTATUS status;
hModule = LoadLibraryExW(L"ShellChromeAPI.dll", NULL, LOAD_LIBRARY_SEARCH_SYSTEM32);
if (hModule == NULL)
{
status = HRESULT_FROM_WIN32(GetLastError());
}
else
{
pFunc = GetProcAddress(hModule, "Shell_RequestShutdown");
if (pFunc == NULL)
{
status = HRESULT_FROM_WIN32(GetLastError());
}
else
{
//Call function in loaded DLL
(*pFunc)(1);
status = 0;
}
FreeLibrary(hModule);
}
return status;
}
The function above normally does nothing since LoadLibraryExW
will return NULL if ShellChromeAPI.dll
is not found.
But it will load and execute the Shell_RequestShutdown
function from that module if it was present in the system folder.
That was the gist of the DLL hijack and the bug itself.
The Override
Then if we look at way how that internal function is called:
//Code snippet that is a part of the RebootHelper::RebootWithFlags function
// ...
if (ShellReboot() >= 0)
{
Sleep(120000); //Wait for 2 minutes
}
dwResult = InitiateShutdownW(NULL, NULL, 0,
SHUTDOWN_FORCE_OTHERS | SHUTDOWN_FORCE_SELF | SHUTDOWN_RESTART | SHUTDOWN_RESTARTAPPS | SHUTDOWN_ARSO,
SHTDN_REASON_FLAG_PLANNED | SHTDN_REASON_MAJOR_OPERATINGSYSTEM | SHTDN_REASON_MINOR_SERVICEPACK);
// ...
It became clear that by providing our own version of the ShellChromeAPI.dll
module with our own exported function Shell_RequestShutdown
,
we can technically control whether or not the subsequent call to InitiateShutdown
succeeds. Or, in other words, we can control whether or not Windows can automatically reboot someone's computer.
This allowed us to kill two birds with one stone:
- Patch the vulnerability of a missing DLL.
- Provide a functionality to indefinitely delay updates on systems that didn't support it, like Windows 10 Home.
That is how I came up with the idea of a small app, that I called "Windows 10 Update Restart Blocker". (You can download it here.) This blog post will explain the details of its function. And if you are not interested in reading and just want to see how it works, I also provided its full source code at GitHub.
Vulnerability Patch
If you remember, the way our PoC of the proposed exploit worked,
we relied on the technique called "Auto-Elevation Mechanism".
But for that hack to work, the destination file in the system folder, i.e. ShellChromeAPI.dll
must be absent.
So if we simply provide our own version of the ShellChromeAPI.dll
module in the System32
folder with a full admin-only write access,
this will basically fix the vulnerability.
It is that simple. So it really puzzles me why Microsoft couldn't do it themselves? But, oh well, I guess we can do it for them.
Delaying Automatic Reboots
Another function that we can employ in our small app, is the one that could give users full control over when they want to reboot their computers. I know, it sounds kinda strange for anyone who hasn't used Windows 10 lately. 😂 But trust me, there's no bigger annoyance than leaving your laptop running overnight only to find it completely rebooted in the morning with all your work closed, or even worse, with some loss of your unsaved data.
There's a manual workaround for these restarts, as I described in this blog post. But, it involves digging into the guts of the OS, which not everyone can do. Worse still, it is not available on some versions of Windows. Like the "Home" version.
I wrote Windows 10 Update Restart Blocker software to solve these issues for the users. Its workings are quite simple really. It overrides and exports the
Shell_RequestShutdown
function in its own ShellChromeAPI.dll
, and instead of doing whatever Microsoft was expecting it to do originally,
it displays a user message box asking for confirmation of a reboot:
Next let's see some basics of how it's done in code.
Displaying A User Message
Since we are not sure where exactly the Shell_RequestShutdown
function can be called from (remember, it can be called either from a system service, or from a UI process)
we can't display our own dialog box. Instead we need to use a special operating-system-sanctioned way to display the message to the user. Luckily,
WTSSendMessage
is designed to do exactly that:
// BOOL bUI_AllowSound; //Set to TRUE to allow to play warning sound
// int nUI_TimeOutSec; //Time-out before hiding the message box in seconds, or 0 to hide it only after user interacts with it
// WCHAR buffTitle[256]; //Title for the message box
// WCHAR buffMsg[1024]; //Message to be displayed
DWORD dwResponse = -1;
DWORD dwActiveSession = WTSGetActiveConsoleSessionId();
DWORD dwchLnTitle = (DWORD)wcslen(buffTitle) * sizeof(WCHAR);
DWORD dwchLnMsg = (DWORD)wcslen(buffMsg) * sizeof(WCHAR);
if(!WTSSendMessage(WTS_CURRENT_SERVER_HANDLE,
dwActiveSession,
buffTitle,
dwchLnTitle,
buffMsg,
dwchLnMsg,
MB_YESNOCANCEL | MB_DEFBUTTON2 | MB_SYSTEMMODAL | (bUI_AllowSound ? MB_ICONWARNING : 0),
nUI_TimeOutSec,
&dwResponse,
TRUE)) //TRUE to wait for time-out or for the user action
{
//Failed
// ...
}
if(dwResponse == IDYES)
{
//User chose to allow to reboot
// ...
}
By displaying a message box, we effectively postpone a restart until the user interacts with it. Then if the user responds by clicking yes to go ahead with a reboot,
the function that calls Shell_RequestShutdown
(via internal ShellReboot
) returns 0,
which unfortunately triggers a 2 minute delay, after which the operating system component will initiate a restart via a call to InitiateShutdownW
.
We can handle this two-minute delay by displaying a message box to the user, telling them that the restart will happen in 2 minutes (since it is hard-coded in the
Microsoft binary - see the Sleep(120000)
call.) We can also use WTSSendMessage
to display our message,
but in this case we need to instruct it to run asynchronously, or to return without waiting for the user to close our message box.
This part is critical since we need to return control back from Shell_RequestShutdown
to allow the restart to continue.:
//Get current time
SYSTEMTIME st = {};
GetLocalTime(&st);
FILETIME ft = {};
SystemTimeToFileTime(&st, &ft);
//Calculate local time 2 minutes from now
const ULONG uiPendingTimeoutSec = 2 * 60;
*(ULONGLONG*)&ft += uiPendingTimeoutSec * 10000000LL;
FileTimeToSystemTime(&ft, &st);
//Format time when reboot will take place for the user
WCHAR buffWhen[128] = {};
GetTimeFormatEx(LOCALE_NAME_SYSTEM_DEFAULT, 0, &st, NULL, buffWhen, _countof(buffWhen));
buffWhen[_countof(buffWhen) - 1] = 0;
StringCchCopy(buffTitle, _countof(buffTitle), L"Pending Reboot!");
StringCchPrintf(buffMsg, _countof(buffMsg),
L"Your computer will restart at %s ..."
,
buffWhen
);
dwchLnTitle = (DWORD)wcslen(buffTitle) * sizeof(WCHAR);
dwchLnMsg = (DWORD)wcslen(buffMsg) * sizeof(WCHAR);
WTSSendMessage(WTS_CURRENT_SERVER_HANDLE,
dwActiveSession,
buffTitle,
dwchLnTitle,
buffMsg,
dwchLnMsg,
MB_OK | MB_ICONINFORMATION | MB_SYSTEMMODAL,
//Hide this pop-up 2 seconds before the reboot because of the bug
//in this API that may show this popup again when rebooting begins ...
uiPendingTimeoutSec - 2,
&dwDummy,
FALSE); //FALSE to show this message asynchronously!
The code above will generate a user message box similar to this:
Blocking A Restart
Then finally, in case the user picks no to the restart in our user prompt, or cancels the message box, we will need to make sure that the
reboot doesn't happen. Since we do not have any control over what happens after the internal function ShellReboot
returns, when InitiateShutdown
is called
unconditionally, one way to stop InitiateShutdown
from rebooting
the system is to remove the SE_SHUTDOWN_PRIVILEGE
privilege needed for that API.
We can adjust the privilege for the process using a helper function like this:
BOOL AdjustPrivilege(LPCTSTR pPrivilegeName, BOOL bEnable, HANDLE hProcess)
{
BOOL bRes = FALSE;
int nOSError = NO_ERROR;
HANDLE hToken;
TOKEN_PRIVILEGES tkp;
if(!OpenProcessToken(hProcess ? hProcess : GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken))
return FALSE;
if(LookupPrivilegeValue(NULL, pPrivilegeName, &tkp.Privileges[0].Luid))
{
tkp.PrivilegeCount = 1;
tkp.Privileges[0].Attributes = bEnable ? SE_PRIVILEGE_ENABLED : 0;
bRes = AdjustTokenPrivileges(hToken, FALSE, &tkp, 0, (PTOKEN_PRIVILEGES)NULL, 0);
nOSError = GetLastError();
if(bRes)
{
if(nOSError != ERROR_SUCCESS)
bRes = FALSE;
}
}
else
nOSError = GetLastError();
CloseHandle(hToken);
SetLastError(nOSError);
return bRes;
}
So all we need to do in our override, if the user didn't allow us to reboot, is to call:
And then return from Shell_RequestShutdown
, which will cause a later call to InitiateShutdown
to fail with the error code ERROR_ACCESS_DENIED
.
Canceling a reboot that was initiated from the "Restart Now" option in the Settings may generate the following error message:We're having trouble restarting to finish the install. Try again in a little while. If you keep seeing this, try searching the web or contacting support for help. This error code might help: (0x80070005)To reboot computer in this case, simply do it from the Start menu's Power option.
Note that the error code shown above is the direct result of our override removing the SE_SHUTDOWN_PRIVILEGE
privilege and thus denying the API that reboots the system to succeed.
This is the expected behavior.
Settings & Installer
But to be honest, the most amount of code in the Windows 10 Update Restart Blocker project is dedicated to the settings and the MSI installer. (You can read about the settings in the app manual.) The main purpose of settings is to make the message box popup less annoying to the users by providing means to automatically repeat previous actions without showing it.
And as the MSI installer is concerned, I wrote it to be universal in a sense that it should install on both, a 32-bit and on a 64-bit OS.
But this is a slight hack of course, since MSI by definition does not support both bitnesses in one .msi
file.
My approach to making my MSI universal was by compiling the MSI file itself as 32-bit and by including all the installation logic in the custom action DLL, which determines the bitness of the OS and installs all appropriate components of the app based on it. This will allow a single MSI to be used on a 32-bit as well as on a 64-bit platform.
Additionally, I made my MSI to support unattended installation via a configuration file which could be handy for installation on multiple computers in an Active Directory environment.
Conclusion
If you are interested in the Windows 10 Update Restart Blocker software:
- You can download the installer here.
- Or, you can check the source code here.
Lastly, Rbmm wrote his own alternative to this patch, that you can read about in the part 2 for this blog post.