Windows secure desktop: Win32 and WPF

A secure desktop is the sort of thing you see when the UAC kicks in and asks you for permissions extraordinaire. Using a secure desktop, or to be precise, a separate desktop for sensitive inputs like passwords, can offer a certain amount of protection against those evil, softwarebased keyloggers. In order to create such a desktop, we need to use a few Win32 API functions.

We’ll start by using the Win32 functions in question in their native tongue, C, because that will introduce us to the bare essentials of desktop switching before we move on to C# and WPF.

Bare metal Win32 API and C

Getting input from a dialog on a separate desktop basically entails creating the desktop and switching to it, plus creating a thread to be associated with it:

struct ThreadData {
    HDESK hNewDesktop;
    // Members for dialog data etc here...
};

void DialogInSecureDesktop(HWND hWnd) {
    DWORD dwDesiredAccess =
        DESKTOP_READOBJECTS |
        DESKTOP_CREATEWINDOW |
        DESKTOP_CREATEMENU |
        DESKTOP_WRITEOBJECTS |
        DESKTOP_SWITCHDESKTOP;

    HDESK hOldDesktop = GetThreadDesktop(GetCurrentThreadId());
    HDESK hNewDesktop = CreateDesktop(L"My desktop", NULL, NULL, 0, dwDesiredAccess, NULL);
    SwitchDesktop(hNewDesktop);

    ThreadData threadData;
    threadData.hNewDesktop = hNewDesktop;

    HANDLE hThread = CreateThread(NULL, 0, SecureDesktopThread, &threadData, 0, 0);
    WaitForSingleObject(hThread, INFINITE); // Wait for the thread to complete

    SwitchDesktop(hOldDesktop); // Restore the original desktop
    CloseDesktop(hNewDesktop);
}

SecureDesktopThread, the function we execute in the thread we created with CreateThread, associates its thread with the new, secure desktop and runs our secure dialog in it:

DWORD WINAPI SecureDesktopThread(LPVOID lpParameter)
{
    ThreadData *threadData = (ThreadData *) lpParameter;
    SetThreadDesktop(threadData->hNewDesktop);
    DialogBox(hInst, MAKEINTRESOURCE(IDD_SECURE_DIALOG), NULL, YourSecureDialogProc);

    return 0;
}

Moving on to WPF and C#

For reasons unfathomable to me, .NET doesn’t provide us with predefined DllImports of the Win32 API, so we all have to invent that wheel over and over again by defining the following imports and constants:

using System.Runtime.InteropServices;

// ...

    // DESKTOP_ACCESS
    const UInt32 DESKTOP_NONE            = 0x0000u;
    const UInt32 DESKTOP_READOBJECTS     = 0x0001u;
    const UInt32 DESKTOP_CREATEWINDOW    = 0x0002u;
    const UInt32 DESKTOP_CREATEMENU      = 0x0004u;
    const UInt32 DESKTOP_HOOKCONTROL     = 0x0008u;
    const UInt32 DESKTOP_JOURNALRECORD   = 0x0010u;
    const UInt32 DESKTOP_JOURNALPLAYBACK = 0x0020u;
    const UInt32 DESKTOP_ENUMERATE       = 0x0040u;
    const UInt32 DESKTOP_WRITEOBJECTS    = 0x0080u;
    const UInt32 DESKTOP_SWITCHDESKTOP   = 0x0100u;

    [DllImport("kernel32.dll")] static extern uint GetCurrentThreadId();
    [DllImport("user32.dll")]   static extern IntPtr GetThreadDesktop(uint dwThreadId);
    [DllImport("user32.dll")]   static extern IntPtr CreateDesktop(string desktopName, IntPtr device, IntPtr deviceMode, uint flags, uint accessMask, IntPtr attributes);
    [DllImport("user32.dll")]   static extern bool SetThreadDesktop(IntPtr hDesktop);
    [DllImport("user32.dll")]   static extern bool CloseDesktop(IntPtr hDesktop);
    [DllImport("user32.dll")]   static extern bool SwitchDesktop(IntPtr hDesktop);

With our imports defined, we can now use almost the exact same logic we used in the C code. One notable exception is that we must create the thread for out new desktop before we start it, because we need to change its apartment state to ApartmentState.STA in order to keep WPF happy:

    static void SecureDialog()
    {
        const UInt32 dwDesiredAccess =
            DESKTOP_READOBJECTS  |
            DESKTOP_CREATEWINDOW |
            DESKTOP_CREATEMENU   |
            DESKTOP_WRITEOBJECTS |
            DESKTOP_SWITCHDESKTOP;

        var hOldDesktop = GetThreadDesktop(GetCurrentThreadId());
        var hNewDesktop = CreateDesktop("My desktop", IntPtr.Zero, IntPtr.Zero, 0, dwDesiredAccess, IntPtr.Zero);
        SwitchDesktop(hNewDesktop);

        var thread = new Thread( () => SecureDesktopThread(hNewDesktop) );
        thread.SetApartmentState(ApartmentState.STA);
        thread.Start();
        thread.Join();  // Wait for thread to complete.

        SwitchDesktop(hOldDesktop);
        CloseDesktop(hNewDesktop);
    }

The thread associated with the new desktop is also very reminiscent of the C version. In case we need one, we must create a synchronization context for the thread (though obviously, this sample doesn’t use one). Doing so requires referencing Dispatcher.CurrentDispatcher, which in turn will create a new dispatcher for this thread. The dispatcher must be shut down when we exit the thread, by Dispatcher.CurrentDispatcher.InvokeShutdown().

    static void SecureDesktopThread(IntPtr hNewDesktop)
    {
        SetThreadDesktop(hNewDesktop);

        SynchronizationContext.SetSynchronizationContext(
            new DispatcherSynchronizationContext(Dispatcher.CurrentDispatcher));

        var win = new Window() { Content = "Hello World!", FontSize=30 };
        win.ShowDialog();
        Dispatcher.CurrentDispatcher.InvokeShutdown();
    }

The complete listing is available as a GitHub Gist.

And now for the bad news: WPF on a secure desktop only works in Windows 10. According to Microsoft, “…this should be a limit of the wpf rendering mechanism. As far as I know, the wpf team has considered this issue as a feature request, and will implement it in future release” (forum thread from 2011).

Because of this, those of you who need to be compatible with Windows 7 and 8 must resort to the dusty, old Windows Forms, which is beyond the scope of this blog post. And just in case anyone wants to wander into that territory: Windows Forms can coexist with WPF in the same application.

  1. No comments yet.

  1. No trackbacks yet.