The Wiert Corner – irregular stream of stuff

Jeroen W. Pluimers on .NET, C#, Delphi, databases, and personal interests

  • My badges

  • Twitter Updates

  • My Flickr Stream

  • Pages

  • All categories

  • Enter your email address to subscribe to this blog and receive notifications of new posts by email.

    Join 4,265 other subscribers

Script alternatives to the Windows-L keyboard shortcut (SwitchUser / LockWorkstation)

Posted by jpluimers on 2024/05/23

More than a decade ago I wrote about Programmatic alternatives to Windows-L keyboard shortcut (SwitchUser / LockWorkstation).

Still, I see many scripts invoke rundll32.exe or  to call the [Wayback/Archive] LockWorkStation function (winuser.h) inside user32.dll. Don’t!

The BOOL LockWorkStation()function has a calling convention that is incompatible with rundll32.exe () which will corrupt the call stack likely will lead to random problems as after two decades, this post from Raymond Chen still holds: [Wayback/Archive] What can go wrong when you mismatch the calling convention? – The Old New Thing

Rundll32.exe entry points

The function signature required for functions called by rundll32.exe is documented in this Knowledge Base article. That hasn’t stopped people from using rundll32 to call random functions that weren’t designed to be called by rundll32, like user32 LockWorkStation or user32 ExitWindowsEx.

Let’s walk through what happens when you try to use rundll32.exe to call a function like ExitWindowsEx:

The rundll32.exe program parses its command line and calls the ExitWindowsEx function on the assumption that the function is written like this:

void CALLBACK ExitWindowsEx(HWND hwnd, HINSTANCE hinst,
       LPSTR pszCmdLine, int nCmdShow);

But it isn’t. The actual function signature for ExitWindowsEx is

BOOL WINAPI ExitWindowsEx(UINT uFlags, DWORD dwReserved);

What happens? Well, on entry to ExitWindowsEx, the stack looks like this:

.. rest of stack ..
nCmdShow
pszCmdLine
hinst
hwnd
return address <- ESP

However, the function is expecting to see

.. rest of stack ..
dwReserved
uFlags
return address <- ESP

What happens? The hwnd passed by rundll32.exe gets misinterpreted as uFlags and the hinst gets misinterpreted as dwReserved. Since window handles are pseudorandom, you end up passing random flags to ExitWindowsEx. Maybe today it’s EWX_LOGOFF, tomorrow it’s EWX_FORCE, the next time it might be EWX_POWEROFF.

Now suppose that the function manages to return. (For example, the exit fails.) The ExitWindowsEx function cleans two parameters off the stack, unaware that it was passed four. The resulting stack is

.. rest of stack ..
nCmdShow (garbage not cleaned up)
pszCmdLine <- ESP (garbage not cleaned up)

Now the stack is corrupted and really fun things happen. For example, suppose the thing at “.. rest of the stack ..” is a return address. Well, the original code is going to execute a “return” instruction to return through that return address, but with this corrupted stack, the “return” instruction will instead return to a command line and attempt to execute it as if it were code.

The not so cool thing is that despite this still failing, all but one of the archived links in the above post by Raymond Chen still work due to link rot.

Fixed links

While writing this in 2022, hopefully while posting in 2024 these links still work, but like always I have included the archived versions as well:

  1. [Wayback/Archive] rundll32 | Microsoft Docs (which lacks crucial information)
  2. [Wayback/Archive: Brian Desmond’s Blog – Shortcut to Lock Computer on Win2k] (which is OK not to be on-line any more as it coined the unsafe rundll32.exe solution)
  3. [Wayback/Archive] Create Lock Desktop Icon (which is bad as it still shows the unsafe rundll32.exe solution)

The first fixed link above show another internet problem besides link rot, namely information loss.

Fewer and fewer people are familiar with old documentation that they decide to update it while throwing away crucial parts maybe thinking that part is still covered elsewhere, while the maintainer in the part elsewhere things the same: [Wayback/Archive] assumption is the mother of all (insert your favourite swear word here).

That is why I quoted the most important bits of the archived [Wayback/Archive] INFO: Windows Rundll and Rundll32 Interface

This article was previously published under Q164787

Summary

Microsoft Windows 95, Windows 98, and Windows Millennium Edition (Me) contains two command-line utility programs named Rundll.exe and Rundll32.exe that allow you to invoke a function exported from a DLL, either 16-bit or 32-bit. However, Rundll and Rundll32 programs do not allow you to call any exported function from any DLL. For example, you can not use these utility programs to call the Win32 API (Application Programming Interface) calls exported from the system DLLs. The programs only allow you to call functions from a DLL that are explicitly written to be called by them. This article provides more details on the use of Rundll and Rundll32 programs under the Windows operating systems listed above.

MIcrosoft Windows NT 4.0, Windows 2000, and Windows XP ship with only Rundll32. There is no support for Rundll (the Win16 utility) on either platform.

The Rundll and Rundll32 utility programs were originally designed only for internal use at Microsoft. But the functionality provided by them is sufficiently generic that they are now available for general use. Note that Windows NT 4.0 ships only with the Rundll32 utility program and supports only Rundll32.

More information

Rundll vs. Rundll32

Rundll loads and runs 16-bit DLLs, whereas Rundll32 loads and runs 32-bit DLLs. If you pass the wrong type of DLL to Rundll or Rundll32, it may fail to run without indicating any error messages.

Rundll command line

The command line for Rundll is as follows:

RUNDLL.EXE <dllname>,<entrypoint> <optional arguments>

How to Write Your DLL

In your DLL, write the <entrypoint> function with the following prototype:

32-bit DLL:

void CALLBACK EntryPoint(HWND hwnd, HINSTANCE hinst, LPSTR lpszCmdLine, int nCmdShow);

The parameters to the Rundll entry point are as follows:

  1. hwnd – window handle that should be used as the owner window for
    any windows your DLL creates
  2. hinst – your DLL’s instance handle
  3. lpszCmdLine – ASCIIZ command line your DLL should parse
  4. nCmdShow – describes how your DLL’s windows should be displayed

In the following example:

RUNDLL.EXE SETUPX.DLL,InstallHinfSection 132 C:\WINDOWS\INF\SHELL.INF

Rundll would call the InstallHinfSection() entrypoint function in Setupx.dll and pass it the following parameters:

  1. hwnd = (parent window handle)
  2. hinst = HINSTANCE of SETUPX.DLL
  3. lpszCmdLine = "132 C:\WINDOWS\INF\SHELL.INF"
  4. nCmdShow = (whatever the nCmdShow was passed to CreateProcess)

The above quotes are from the archived 2016 article which more or less has recnognisable bits of Q164787 as base part: support.microsoft.com/en-us/kb/164787.

As of 2017, the URL got changed substantially into [Wayback] support.microsoft.com/en-us/help/164787/info-windows-rundll-and-rundll32-interface as part of a large support.microsoft.com site overhaul (also causing most Wayback Machine and archive.is sites to become empty).

Then in 2020, support.microsoft.com got overhauled and integrated into docs.microsoft.com as

[Wayback/Archive] rundll32 | Microsoft Docs, and also shortening title, content and URL changed to reflect the title: docs.microsoft.com/en-us/windows-server/administration/windows-commands/rundll32.

The only Microsoft documentation article (via [WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW/Archive] ncmdshow rundll32 EntryPoint site:microsoft.com – Google Search) now explaining the function declaration expected by rundll32 is [Wayback/Archive] PassportWizardRunDll function – Win32 apps | Microsoft Docs:

[This function is available through Windows XP with Service Pack 2 (SP2) and Windows Server 2003. It might be altered or unavailable in subsequent versions of Windows.]

Launches the Passport Wizard when used with Rundll32.exe.

Syntax

void PassportWizardRunDll( _In_ HWND hwndStub, _In_ HINSTANCE hAppInstance, _In_ LPTSTR lpszCmdLine, _In_ int nCmdShow );

Remarks

Using PassportWizardRunDll as an entry point into the Netplwiz.dll file through a Rundll32 command allows you to launch the Passport Wizard from a command line as though it were an executable file.

PassportWizardRunDll is used solely in the context of a Rundll32.exe command as follows:

rundll32.exe netplwiz.dll, PassportWizardRunDll

Using an entry point function with Rundll32.exe does not resemble a normal function call. The function name and the name of the .dll file where it is stored are used only as command-line parameters. The function definition shown under Syntax is only a standard prototype for all functions that you can call using Rundll32. The specific values for hwndStub, hAppInstance, and nCmdShow are not provided by the user, but are handled behind the scenes by Rundll32. PassportWizardRunDll does not use the lpszCmdLine value, so no additional data is required.

Problem

From a script like a batch file, execute the LockWorkstation API function in user32.dll that is incompatible with what rundll32.exe expects.

Solution

Back when writing the original blog-post, few tools wrapped the LockWorkstation API.

In the mean time, by default Windows ships with sufficiently recent versions of C# and PowerShell and both [Wayback/Archive] PsTools and NirCmd are readily available through Chocolatey.

C# solution

The fastes solution is likely to compile this with any csc.exe (C# compiler) on your path once, then execute the resulting LockWorkstation.exe:

using System.ComponentModel;
using System.Runtime.InteropServices;

namespace LockWorkstation
{
    class Program
    {
        // https://docs.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.dllimportattribute
        [DllImport("user32.dll", SetLastError = true)]
        static extern bool LockWorkStation();

        static void Main()
        {
            // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-lockworkstation
            if (!LockWorkStation()) // not sure when this could fail, but just in case:
                throw new Win32Exception(Marshal.GetLastWin32Error()); // https://docs.microsoft.com/en-us/dotnet/api/system.componentmodel.win32exception
        }
    }
}

On my system compiling is through this:

%windir%\Microsoft.NET\Framework64\v4.0.30319\csc.exe LockWorkstation.cs

It leaves LockWorkstation.exe which you can execute and book the workstation is locked, even in an RDP session (which is cool as you can lock sessions you do not immediately need but still keep them active so it is easier to go back to them).

I built the above solution based on:

PowerShell solution

PowerShell has gained a lot of functionality including the possibility to import external functions similar do DllImport in C#.

From the links below, I first created this PowerShell script that has the correct double quotes and tested it:

function Lock-WorkStation {
  $signature =
@"
  [DllImport("user32.dll", SetLastError = true)]
  public static extern bool LockWorkStation();
"@

  $LockWorkStation = Add-Type -memberDefinition $signature -name "Win32LockWorkStation" -namespace Win32Functions -passthru
  $LockWorkStation::LockWorkStation() | Out-Null
}

Lock-WorkStation

Then I condensed into this:

function Lock-WorkStation { $signature = '[DllImport("user32.dll", SetLastError = true)] public static extern bool LockWorkStation();'; $LockWorkStation = Add-Type -memberDefinition $signature -name "Win32LockWorkStation" -namespace Win32Functions -passthru; $LockWorkStation::LockWorkStation() | Out-Null }
Lock-WorkStation

and further into this:

(Add-Type -memberDefinition '[DllImport("user32.dll", SetLastError = true)] public static extern bool LockWorkStation();' -name "Win32LockWorkStation" -namespace Win32Functions -passthru)::LockWorkStation() | Out-Null

Indeed: it is a 200+ character line, but now everything is on one line, so now I was able to put it inside the below batch file as a PowerShell call by escaping all " into \":

:: https://wiert.wordpress.com/?p=107892

:: requires administrative UAC elevation:
psshutdown -l

:: NirCmd can run without elevation:
nircmd lockws

:: PowerShell can run without elevation:

PowerShell "(Add-Type -memberDefinition '[DllImport(\"user32.dll\", SetLastError = true)] public static extern bool LockWorkStation();' -name \"Win32LockWorkStation\" -namespace Win32Functions -passthru)::LockWorkStation() | Out-Null"

And yes, the last line is still below 240 characters, so [Wayback/Archive] Jeroen Wiert Pluimers on Twitter: It fits in a Tweet! PowerShell "(Add-Type -memberDefinition '[DllImport(\"user32.dll\", SetLastError = true)] public static extern bool LockWorkStation();' -name \"Win32LockWorkStation\" -namespace Win32Functions -passthru)::LockWorkStation() | Out-Null"

I built the above solution based on:

Batch file solution

One application that can call the LockWorkstation function is PsShutdown, however it requires an administrative UAC elevation token, so it usually cannot be called from batch-files.

NirCmd does not require UAC elevation and also executes quickly , but sometimes is disallowed to be installed (for instance by stringent antivirus software).

It is included as it is the fastest way. If it fails or not installed, the below batch file uses the above PowerShell solution.

Note:

I built the above solution based on the above PowerShell solution and these links:

zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz

Similar alternative

In my previous article, I mentioned tsdiscon.exe which kind of locks your workstation but – if you are on a Remote Desktop Connection – also terminates that connection. For most people that certainly is not the preferred behaviour.

–jeroen

 

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.