One day, during the winter break away from work, I was reminded of this ancient program I built in high school—a Blue Screen of Death (henceforth BSoD) simulator. It really has been a while since I’ve covered coding topics on this blog, so I thought it might be time to dig up the program and resurrect it.

But you might ask the very natural question: why did I write such a program? For that, I blame the school. You see, I had to deal with school computers a lot back in the day, not being one of those “cool kids” who brought their own laptops to school. Naturally, the school computers ran Windows XP1. For some reason, the school administrators decided to deploy group policy to deny screen locking.

This posed a conundrum: if I had to step away from the computer—like say, answering the call of nature—I was confronted with two deeply uncomfortable choices:

  1. Save all my work, close all applications, and log out, then wait forever to log back in on those slow computers, while hoping that no one else took the fastest one of the bunch2; or
  2. Leave the computer unattended, and if someone does something sketchy as a prank, I’d be the one in trouble.

One day, I saw a school computer stuck with a BSoD, and no one ever touched it, and that gave me an idea: what if I wrote my own custom “lock screen” program that masqueraded as a BSoD? And thus was the program born.

What did it do?

The program obviously needed to display a BSoD in full screen mode. The first iteration of the program simply loaded the PNG downloaded from Wikipedia, specifically this picture. It was very easy to tell after a while that the screen was fake due to it looking the same every time. It really doesn’t help that the current Unix time3 in hexadecimal is shown at the very end. For example, the Wikipedia picture says DateStamp 3d6dd67c, which is 1,030,608,508 seconds after the Unix epoch, or 2002-08-29 08:08:28 UTC. Given that in the 2010s, no timestamp should start with 3 in hexadecimal, this easily gave the game away to those who understand.

So naturally, the program had to render an actual BSoD, generating random but believable values for every field, using the current time in the DateStamp field.

It also needed several features to be effective as a lock screen, like completely blocking access to the system while it’s running, or what’s the point? So it had to:

  1. be a topmost window in addition to being full screen, blocking all regular windows from view;
  2. prevent random system popups, such as the built-in sticky keys prompt, which inevitably showed up when testers spammed the keyboard;
  3. prevent the task manager from being launched to kill the program;
  4. prevent the program from being killed some other way;
  5. block most keyloggers through the use of a “secure desktop”, since I’d be typing in a password;
  6. prevent someone from logging me out without my authorization; and
  7. prevent the system from being shut down (except by pulling the plug).

The exact way these functionalities are accomplished will be described later, when we dive into the implementation details.

What it can’t do is intercept Ctrl+Alt+Delete, which is handled by winlogon.exe with no exceptions. Microsoft specifically designed it as a “secure attention sequence” to help the user confirm that they are typing their password into the real login screen and not a fake program phishing for the password. On school computers, pressing Ctrl+Alt+Delete shows a screen asking the user for various options, which unfortunately could not be blocked and looked something like this:

Windows XP security screen

(Naturally, the “Lock Computer” option was greyed out on the actual school computers, or I wouldn’t have to write this program.)

Still, it did the best it could, and surprisingly few people know about the intricacies of Ctrl+Alt+Delete

The development environment

Programs are the product of their environment, and my BSoD program is no exception. For something that will need to call a lot of Windows API calls directly, any higher-level language like Python or Java is automatically out. The obvious candidates were C and C++.

I wanted to be able to develop this program on a school computer, in case I found a bug that I wanted to fix immediately without waiting to go home, so it had to work on a tiny toolchain that I could bring to school and not take up too much space. For this purpose, I chose the Microsoft Visual C++ 6.0 toolchain (henceforth VC6) from 19984, which, after debloating, compressed down to around 9 MiB with 7-Zip.

In this vein, I tried to make the final executable as lightweight as possible, so C++ was automatically out. That meant I had to write this program in C, and since VC6 pre-dated the C99 standard, the only acceptable form of C was C89. This had several annoying quirks, such as requiring all variable declarations to be hoisted manually to the top of every scope, no exceptions, or requiring the loop variable of a for-loop to be declared separately, but was otherwise reasonably similar to modern C.

Since the compiler predated 64-bit Windows, and the school computers were exclusively 32-bit anyway, no attempt was made to support compiling in 64-bit mode. The program would run fine on 64-bit Windows anyway through WoW64.

VC6 also had the advantage of supporting dynamic linking for a smaller executable size without requiring a runtime redistributable to be installed, which often isn’t the case for newer versions of Visual C++, and whether each school computer had each redistributable was completely random. However, when dynamically linking the C runtime on VC6, it simply used msvcrt.dll, which is part of the operating system. It’s quite understandable why Microsoft gave up on this approach, since it made it impossible to add newer features without causing “DLL hell” or application compatibility issues, but still… Naturally, it was possible to statically link the C library, but that added to the executable size.

Although in the end, none of the C library linking stuff ended up mattering, because I decided to ditch the C library to achieve the minimum possible executable size…

Raw Windows API programming

Most people who have written a Windows GUI program would know that the program entry point is:

int WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd);

However, this is not strictly true. As Raymond pointed out, this is actually a function provided by the C runtime library to replicate the behaviour of 16-bit Windows, and the actual real entrypoint for the program is

DWORD APIENTRY RawEntryPoint(void);

Partly inspired by that blog post, this program did the insane thing of ditching the C library and programming directly to RawEntryPoint. This meant that no C library function could be used at all, only Windows APIs. Due to choosing C instead of C++, this was actually possible to accomplish, as I would otherwise have to avoid most C++ features like the plague due to the runtime being implicitly used in most of them.

On newer compilers, several security features, such as guards against stack buffer overflows, had to be turned off, as those require runtime support. Fortunately, VC6 didn’t have those features anyway…

In retrospect, this was totally not worth it, but that’s how the program was constructed. Clearly, I had too much time in high school…

Seeing the program in action

You can download the latest source code from GitHub and compile it yourself, if you have a copy of Visual C++ lying around. Most versions should work, but for the authentic experience, you should use VC6. A VC6-compiled binary is available on my Jenkins instance5.

It should look like this, with a random driver and error code selected:

Screenshot of bsod.exe

To exit, hold down Alt and press the following keys in sequence: F2, F4, F6, F8, 1, 3, 5, 7. Then, press Ctrl+Alt+Shift+Delete.

Enter the password quantum5.ca to exit.

Lessons learned

Given that 13 years have gone by since the program was made, it’s inevitable that I would probably have done things differently now compared to then.

Code-wise, I didn’t have to make too many changes to satisfy my current self. The biggest issue was using funny magic numbers instead of defining them as proper constants, but that was easily rectified. I would probably not have ditched the C library writing this now, but it really wasn’t that big of a burden for this application, as you shall see.

The real lesson lies in the non-coding portion, and underscores how writing code is probably the easiest part of software development. There are many things I would have done differently:

  1. Using source control. Apparently, my younger self didn’t bother with any kind of source control and just emailed the source code around in plain text. This made it quite difficult to determine the canonical latest version of this program. It is entirely possible that a version with more fixes here got lost over the years. These days, it’s completely unimaginable for me to not use Git to manage everything.
  2. Using a proper build system, even if it’s just Makefiles. Apparently, my younger self just had a command line to compile the program written down separately, which depended on a bunch of generated files that were not obvious how to regenerate, such as a compiled .res file. This required a bit of forensic work to reconstruct the .rc file so I could compile it again. Even worse, the flags to compile the source under VC6 were written down in a completely different place compared to newer versions of the compiler… It really didn’t help that the source code was sent around as text in emails, without any supporting files. In the modernized Git repo, there is a proper Makefile, and a separate one for legacy VC6. Both of these will build the program completely from scratch, instead of relying on distributing intermediate files.
  3. Using continuous integration. Apparently, my younger self had a ton of slightly different executables floating around in a directory somewhere, and it’s not clear which one is the “good” one. This was clearly insane. The modernized version has GitHub Actions set up to build every commit to ensure the repository successfully compiles under modern compilers, without dependency on any local, uncommitted files. I also have Jenkins set up specifically with VC6 to generate builds with the authentic original toolchain for every commit, ensuring that the latest executable is always available.
  4. Using an autoformatter, like clang-format. My younger self decided to manually format all the code, resulting in a coding style that’s not quite consistent. While it doesn’t matter as much for a single-person project like this, having an autoformatter is essential in larger projects involving multiple people to avoid lots of different styles mixing and blending together. The current GitHub version has a .clang-format file defining the expected style.

Nevertheless, I am pleasantly surprised by my younger self at close to half my current age!

Annotated source code

Before we dive in, it’s important to keep in mind that the program is structured to have a bunch of macros that could be defined at compile time to control the behaviour:

  • NOAUTOKILL, which disables the automatic exiting after 50 seconds logic. This is invaluable when developing; and
  • NOTASKMGR, which disables the task manager. The way it does so is kinda sketchy, so this gives an opt-out.

Without further ado, let’s look at the source code, which canonically resides on GitHub.

Headers

#define _WIN32_WINNT 0x0500
#define WINVER       0x0500

#include <windows.h>

First, we define version macros. In this case, we ask windows.h to make available all functions available in Windows 2000, which has everything we needed.

#include <aclapi.h>
#include <shlwapi.h>

We then include two more Windows header files, one for ACL operations, and the other for the wnsprintf function. This is an obscure sprintf variant that I would never normally use, except sprintf isn’t available due to libc being ditched. Borrowing the version from shlwapi.dll was the natural solution.

Macros

#define ARRAY_SIZE(x) (sizeof(x) / sizeof *(x))

We define this handy ARRAY_SIZE macro so we can just pass in an array and compute its size as a constant. The way this works is by calling sizeof on the whole array to get its size in bytes, then calling sizeof to get the size of the first element of the array in bytes, and dividing. sizeof *(x) yields the size of the first element because arrays decay to the pointer to the first element in most contexts, including dereferencing, so *(x) yields the first element.

This macro will be broken if the argument isn’t an actual C array. If I cared more, I would make a better version that errors out when something else is passed in, but oh well.

#define WM_ENFORCE_FOCUS (WM_APP + 0)

#define TM_DISPLAY   0xBEEF
#define TM_AUTOKILL  0xDEAD
#define TM_FORCEDESK 0xFAC

#define AUTOKILL_TIMEOUT 50000
#define DISPLAY_DELAY    1000
#define FORCE_INTERVAL   1000

#define HDLG_MSGBOX ((HWND) 0xDEADBEEF)
#define IDC_EDIT1   1024

We then define a bunch of constants, including a window message, a bunch of timer messages, a bunch of time intervals, and some other values. Everything other than WM_ENFORCE_FOCUS manifested as magic numbers in the code. I’ve preserved the original values, but gave them proper names.

IDC_EDIT1 is notably duplicated in bsod.rc, the resource file. If I had more than one constant, I would have defined a header and included it in both bsod.rc and bsod.c.

Compatibility

#if defined(_MSC_VER) && _MSC_VER <= 1200
#define wnsprintf wnsprintfA
int wnsprintfA(PSTR pszDest, int cchDest, PCSTR pszFmt, ...);
typedef unsigned char *RPC_CSTR;
#endif

To ensure it builds on the ancient VC6 with its outdated Windows SDK headers, we define the prototype of wnsprintf, which is actually wnsprintfA in the DLL due to it being the non-Unicode version. Windows headers normally define a macro to replace the name with either the A or W version, depending on whether the UNICODE macro is defined.

We also define the RPC_CSTR type, since we’ll be using an RPC function later, and that type is somehow missing in VC6.

typedef BOOL(WINAPI *LPFN_SHUTDOWNBLOCKREASONCREATE)(HWND, LPCWSTR);
typedef BOOL(WINAPI *LPFN_SHUTDOWNBLOCKREASONDESTROY)(HWND);

This defines the function pointer types for ShutdownBlockReasonCreate and ShutdownBlockReasonDestroy functions introduced in Windows Vista, in anticipation of the school upgrading to Windows 7, which did happen eventually. These will be needed to actually block shutdown on newer versions of Windows.

Function prototypes

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
INT_PTR CALLBACK DlgProc(HWND, UINT, WPARAM, LPARAM);
LRESULT CALLBACK LowLevelKeyboardProc(int, WPARAM, LPARAM);
LRESULT CALLBACK LowLevelMouseProc(int, WPARAM, LPARAM);

We now define a bunch of function prototypes.

Constants

#define PASSWORD_LENGTH (sizeof(szRealPassword) - 1)
const char szRealPassword[] = {0x54, 0x50, 0x44, 0x4b, 0x51, 0x50,
                               0x48, 0x10, 0xb,  0x46, 0x44, 0x00};
const char szClassName[] = "BlueScreenOfDeath";

This defines two constants, one for the encoded password, and one for the window class name.

The password is quantum5.ca, encoded by XORing with the number 37. I don’t remember why it was chosen, other than perhaps because it’s a prime number.

Variables

We now declare a bunch of global variables:

HINSTANCE hInst;
HWND hwnd;  // Main window
HWND scwnd; // Static bitmap control
HWND hdlg;  // Password popup

HACCEL hAccel;
HHOOK hhkKeyboard, hhkMouse;
HDESK hOldDesk, hNewDesk;
char szDeskName[40];

LPFN_SHUTDOWNBLOCKREASONCREATE fShutdownBlockReasonCreate;
LPFN_SHUTDOWNBLOCKREASONDESTROY fShutdownBlockReasonDestroy;

Note that using global variables like this is not recommended, but in this case, there’s no way one instance of this program will ever show more than one window, so whatever.

hInst exists solely because a bunch of Windows APIs ask for HINSTANCE due to 16-bit compatibilty reasons. Normally, you’d save the hInstance passed to WinMain, but we don’t have that. You’ll see how we calculate it later.

The HWND variables are self-explanatory.

The HACCEL is for storing a handle to the accelerators, and we will be using keyboard accelerator tables instead of implementing complex parsing logic for keystrokes.

HHOOK variables are used to create low-level keyboard and mouse hooks.

HDESK variables are used to store the handle to the original desktop and a new “secure desktop.” szDeskName stores the name of the secure desktop.

We also define variables to store the pointers to ShutdownBlockReasonCreate and ShutdownBlockReasonDestroy functions, if they exist.

#ifdef NOTASKMGR
HKEY hSystemPolicy;
#endif

If the task manager is to be disabled, we define an HKEY to store a handle to a certain registry key.

Keyboard accelerator table

ACCEL accel[] = {
    {FALT | FVIRTKEY,                     '1',       0xBE00},
    {FALT | FVIRTKEY,                     '3',       0xBE01},
    {FALT | FVIRTKEY,                     '5',       0xBE02},
    {FALT | FVIRTKEY,                     '7',       0xBE03},
    {FALT | FVIRTKEY,                     VK_F2,     0xBE04},
    {FALT | FVIRTKEY,                     VK_F4,     0xBE05},
    {FALT | FVIRTKEY,                     VK_F6,     0xBE06},
    {FALT | FVIRTKEY,                     VK_F8,     0xBE07},
    {FALT | FCONTROL | FSHIFT | FVIRTKEY, VK_DELETE, 0xDEAD},
};
BOOL bAccel[ARRAY_SIZE(accel) - 1];

Here, we define a keyboard accelerator table, uncreatively called accel. This will eventually be passed to CreateAcceleratorTable. We define 8 shortcut keys to be pressed, generating WM_COMMAND messages with codes 0xBE00 to 0xBE07 in wParam. These can be triggered by holding down Alt and then pressing the 1, 3, 5, 7, F2, F4, F6, and F8 keys. bAccel tracks whether any of these keys were pressed.

Finally, there’s a shortcut for Ctrl+Alt+Shift+Delete, which triggers a WM_COMMAND with 0xDEAD, that signals the program to trigger the exit routine.

Helper functions

void GenerateUUID(LPSTR szUuid) {
    UUID bUuid;
    RPC_CSTR rstrUUID;

    UuidCreate(&bUuid);
    UuidToString(&bUuid, &rstrUUID);
    lstrcpy(szUuid, (LPCSTR) rstrUUID);
    RpcStringFree(&rstrUUID);
}

This function generates a UUID, which will be used to create a random and guaranteed-to-be-unique name for a secure desktop. It uses a bunch of functions from rpcrt4.dll, like UuidCreate and UuidToString, to generate the UUID, and we use lstrcpy to copy it into the buffer passed in, before freeing the weird RPC_CSTR. Note that lstrcpy from kernel32.dll is used because we can’t use strcpy in the C library.

int UnixTime() {
    union {
        __int64 scalar;
        FILETIME ft;
    } time;
    GetSystemTimeAsFileTime(&time.ft);
    return (int) ((time.scalar - 116444736000000000i64) / 10000000i64);
}

This is effectively a reimplementation of the time function from the C standard library, which we can’t use. So instead, we use GetSystemTimeAsFileTime, which returns the current time as a 64-bit integer, divided into low and high parts as 32-bit DWORDs in the FILETIME structure. To make it easily interpretable, we convert it to __int64—Microsoft’s extension type for 64-bit integer in the days before long long is properly supported—through a union, which is the safe way to ensure it’s properly aligned.

Note that FILETIME is defined as measuring the time in 100 ns intervals since midnight UTC on January 1, 1601. To convert this to Unix time, we need to subtract 116,444,736,000,000,000 such intervals, i.e. 11.6 billion seconds, to make the zero point the Unix epoch, and then divide by 10 million to convert to seconds.

Note that this implementation technically uses the Microsoft Visual C++ library function _alldiv to perform the division. However, that file does not introduce any further dependencies on the C library, most notably the initialization code, which is the main source of bloat. However, if you really hate the C library, the following alternative implementation in x86 assembly will avoid it:

__declspec(naked) int UnixTime(void) {
    __asm {
        sub     esp,    8
        push    esp
        call    dword ptr GetSystemTimeAsFileTime
        pop     eax
        pop     edx
        sub     eax,    3577643008
        sbb     edx,    27111902
        mov     ecx,    10000000
        idiv    ecx
        ret
    }
}

But I see no need to make the code unreadable for the exact same executable size… Anyways, let’s go on:

int Random() {
    static int seed = 0;
    if (!seed)
        seed = UnixTime();
    seed = 1103515245 * seed + 12345;
    seed &= 0x7FFFFFFF;
    return seed;
}

We then use this crappy random number generator that uses the current Unix time as a seed. This isn’t going to win any awards or be suitable for cryptographic use, but it’s good enough for this program.

For those curious, this implementation is a linear congruential generator, specifically with the parameters used in glibc, with the output capped to be positive through bitwise AND with 0x7FFFFFFF.

Protection functions

And then we have a bunch of functions to make sure the program doesn’t get killed…

#ifdef NOTASKMGR
void DisableTaskManager(void) {
    DWORD dwOne = 1;
    if (hSystemPolicy)
        RegSetValueEx(hSystemPolicy, "DisableTaskMgr", 0, REG_DWORD, (LPBYTE) &dwOne,
                      sizeof(DWORD));
}

void EnableTaskManager(void) {
    DWORD dwZero = 0;
    if (hSystemPolicy)
        RegSetValueEx(hSystemPolicy, "DisableTaskMgr", 0, REG_DWORD, (LPBYTE) &dwZero,
                      sizeof(DWORD));
}
#endif

These functions disable and enable the task manager by setting a registry key that would normally be controlled by group policy. Specifically, it sets a value named DisableTaskMgr under the key in hSystemPolicy, which will be opened to HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Policies\System.

This is only active if the NOTASKMGR macro is defined.

DWORD ProtectProcess(void) {
    ACL acl;

    if (!InitializeAcl(&acl, sizeof acl, ACL_REVISION))
        return GetLastError();

    return SetSecurityInfo(GetCurrentProcess(), SE_KERNEL_OBJECT, DACL_SECURITY_INFORMATION, NULL,
                           NULL, &acl, NULL);
}

This function creates a blank ACL and sets it on the current process object. Since a blank ACL does not grant any permissions, it means that no one has the permission to perform any operations on this process, for “nefarious” things such as terminating it.

Fun fact: I learned this trick from this crapware that the school had installed on the computers. I had a batch file that killed all the startup programs to squeeze out every drop of performance on the ancient school computers, and that one program couldn’t be killed. Naturally, I studied how it worked, and it did so by updating the ACL to deny everyone PROCESS_TERMINATE, i.e. the ability to call TerminateProcess on it. It had a fatal flaw: it didn’t stop someone from calling CreateRemoteThread on the process. Naturally, I wrote a helper program that created a remote thread in the crapware that ran ExitProcess, and lo and behold, the crapware was successfully killed.

Since I am using a blank ACL here, it uses a lot less code than updating the ACL as used by that crapware, and there’s the added bonus that it denies all sketchy operations, including CreateRemoteThread and any other debugging operations…

Also note that ACLs are supposed to be variable length and heap-allocated, with stuff coming after the ACL structure in the same block of memory, but the degenerate case of the empty ACL fits on the stack just fine.

STICKYKEYS StartupStickyKeys = {sizeof(STICKYKEYS), 0};
TOGGLEKEYS StartupToggleKeys = {sizeof(TOGGLEKEYS), 0};
FILTERKEYS StartupFilterKeys = {sizeof(FILTERKEYS), 0};

void AllowAccessibilityShortcutKeys(BOOL bAllowKeys) {
    if (bAllowKeys) {
        SystemParametersInfo(SPI_SETSTICKYKEYS, sizeof(STICKYKEYS), &StartupStickyKeys, 0);
        SystemParametersInfo(SPI_SETTOGGLEKEYS, sizeof(TOGGLEKEYS), &StartupToggleKeys, 0);
        SystemParametersInfo(SPI_SETFILTERKEYS, sizeof(FILTERKEYS), &StartupFilterKeys, 0);
    } else {
        STICKYKEYS skOff = StartupStickyKeys;
        TOGGLEKEYS tkOff = StartupToggleKeys;
        FILTERKEYS fkOff = StartupFilterKeys;

        if ((skOff.dwFlags & SKF_STICKYKEYSON) == 0) {
            skOff.dwFlags &= ~SKF_HOTKEYACTIVE;
            skOff.dwFlags &= ~SKF_CONFIRMHOTKEY;
            SystemParametersInfo(SPI_SETSTICKYKEYS, sizeof(STICKYKEYS), &skOff, 0);
        }
        if ((tkOff.dwFlags & TKF_TOGGLEKEYSON) == 0) {
            tkOff.dwFlags &= ~TKF_HOTKEYACTIVE;
            tkOff.dwFlags &= ~TKF_CONFIRMHOTKEY;
            SystemParametersInfo(SPI_SETTOGGLEKEYS, sizeof(TOGGLEKEYS), &tkOff, 0);
        }
        if ((fkOff.dwFlags & FKF_FILTERKEYSON) == 0) {
            fkOff.dwFlags &= ~FKF_HOTKEYACTIVE;
            fkOff.dwFlags &= ~FKF_CONFIRMHOTKEY;
            SystemParametersInfo(SPI_SETFILTERKEYS, sizeof(FILTERKEYS), &fkOff, 0);
        }
    }
}

This is the helper function that toggles the Windows accessibility shortcuts, specifically for sticky keys, toggle keys, and filter keys. We define three structures to store the state of each, which will be populated on startup. The helper function, when told to allow, will set those three back to the original state with SystemParametersInfo. When told to disallow, it will remove the bits for the keyboard shortcut and the confirmation dialogue from the flags for each.

Generating the blue screen

LPSTR bsod1 = "\r\n\
A problem has been detected and Windows has been shut down to prevent damage\r\n\
to your computer.\r\n\
\r\n\
The problem seems to be caused by the following file: ";

LPSTR bsod2 = "\r\n\r\n";

LPSTR bsod3 = "\r\n\
\r\n\
If this is the first time you've seen this stop error screen,\r\n\
restart your computer. If this screen appears again, follow\r\n\
these steps:\r\n\
\r\n\
Check to make sure any new hardware or software is properly installed.\r\n\
If this is a new installation, ask your hardware or software manufacturer\r\n\
for any Windows updates you might need.\r\n\
\r\n\
If problems continue, disable or remove any newly installed hardware\r\n\
or software. Disable BIOS memory options such as caching or shadowing.\r\n\
If you need to use Safe Mode to remove or disable components, restart\r\n\
your computer, press F8 to select Advanced Startup Options, and then\r\n\
select Safe Mode.\r\n\
\r\n\
Technical information:\r\n\
\r\n";

LPSTR bsod4 = "*** STOP: 0x%08X (0x%08X,0x%08X,0x%08X,0x%08X)";
LPSTR bsod5 = "\r\n\r\n\r\n***  ";
LPSTR bsod6 = "%s - Address %08X base at %08X, DateStamp %08x";

First, we have a bunch of string fragments that comprise the text shown on the blue screen. These are divided into six chunks, and bsod4 and bsod6 are actually printf-style format strings.

LPSTR lpBadDrivers[] = {
    "HTTP.SYS",  "SPCMDCON.SYS", "NTFS.SYS",   "ACPI.SYS",  "AMDK8.SYS", "ATI2MTAG.SYS",
    "CDROM.SYS", "BEEP.SYS",     "BOWSER.SYS", "EVBDX.SYS", "TCPIP.SYS", "RDPDR.SYS",
};

We then have a bunch of system drivers that could be blamed for the crash. I am not sure why most of these were selected in particular, but I think:

  • ATI2MTAG.SYS was selected because the school computers had ATI6 graphics cards, and the display driver often crashed;
  • BEEP.SYS was selected because I had code in MusicKeyboard that directly interfaced with it; and
  • BOWSER.SYS was selected because it had a weird name that was not a typo.
typedef struct {
    LPSTR name;
    DWORD code;
} BUG_CHECK_CODE;

BUG_CHECK_CODE lpErrorCodes[] = {
    {"INVALID_SOFTWARE_INTERRUPT",  0x07},
    {"KMODE_EXCEPTION_NOT_HANDLED", 0x1E},
    {"PAGE_FAULT_IN_NONPAGED_AREA", 0x50},
    {"KERNEL_STACK_INPAGE_ERROR",   0x77},
    {"KERNEL_DATA_INPAGE_ERROR",    0x7A},
};

Then we had a bunch of common error codes. Note that these are called BUG_CHECK_CODE because “bug check” is the proper name for the BSoD, and in fact, the kernel function that triggers them is called KeBugCheck. We needed both the string name and the internal code, because the code will be shown in the STOP: line.

HBITMAP RenderBSoD(void) {
    HBITMAP hbmp;
    HDC hdc;
    HBRUSH hBrush = CreateSolidBrush(RGB(0, 0, 128));
    RECT rect = {0, 0, 640, 480};
    HFONT hFont;
    char bsod[2048];
    char buf[1024];
    LPSTR lpName;
    BUG_CHECK_CODE bcc;
    DWORD dwAddress;
    int i, k;

Now we have the RenderBSoD function, which returns an HBITMAP containing the rendered image for the BSoD. Note that we hoist all the variable declarations to the top of the function due to C89 limitations.

Most of these variables are self-explanatory or will soon be obvious. The hBrush is the solid blue brush that will be used to paint the screen blue. bsod is the final generated string that we initialize, since it’ll be used.

    // Initialize RNG
    k = Random() & 0xFF;
    for (i = 0; i < k; ++i)
        Random();

We then initialize the random number generator somewhat by taking a random byte and skipping ahead by that many numbers. This makes the output a bit more random.

    hdc = CreateCompatibleDC(GetDC(hwnd));
    hbmp = CreateCompatibleBitmap(GetDC(hwnd), 640, 480);
    hFont = CreateFont(14, 8, 0, 0, FW_NORMAL, 0, 0, 0, ANSI_CHARSET, OUT_RASTER_PRECIS,
                       CLIP_DEFAULT_PRECIS, NONANTIALIASED_QUALITY, FF_MODERN, "Lucida Console");

We then create an HDC that’s compatible with our main window, whose handle will be in hwnd when this function is called. The DC, or device context, defines the attributes of the output device, which in this case is just a screen, and contains some state for rendering parameters.

We then create a 640×480 bitmap that’s similarly compatible, onto which the BSoD will be rendered. Since 640×480 is the screen resolution that Windows falls back to when rendering the actual BSoD, this will make it look correct.

We then create the font. After some experimentation and comparison with a real BSoD, I found that Lucida Console with height 14 and width 8 in GDI logical units looked indistinguishable from the real one. It is very important to pass NONANTIALIASED_QUALITY since there is no smoothing or ClearType™ on the BSoD, and this ensures that the text looks as ugly as it does on the real screen.

    lstrcpy(bsod, bsod1);
    lpName = lpBadDrivers[Random() % ARRAY_SIZE(lpBadDrivers)];
    bcc = lpErrorCodes[Random() % ARRAY_SIZE(lpErrorCodes)];
    lstrcat(bsod, lpName);
    lstrcat(bsod, bsod2);
    lstrcat(bsod, bcc.name);
    lstrcat(bsod, bsod3);

We populate bsod from the string fragments, inserting the driver name and the error code string as necessary. The random number generator is used to pick a random driver and code from the lists.

Note that we start the string with lstrcpy and then add onto it with lstrcat. These are just the kernel32.dll versions of strcpy and strcat, which we can’t use due to shunning the C library.

    switch (Random() % 4) {
    case 0:
        wnsprintf(buf, ARRAY_SIZE(buf), bsod4, bcc.code, Random() | 1 << 31, Random() & 0xF,
                  Random() | 1 << 31, 0);
        break;
    case 1:
        wnsprintf(buf, ARRAY_SIZE(buf), bsod4, bcc.code, Random() | 1 << 31, Random() | 1 << 31,
                  Random() & 0xF, Random() & 0xF);
        break;
    case 2:
        wnsprintf(buf, ARRAY_SIZE(buf), bsod4, bcc.code, Random() | 1 << 31, 0, Random() & 0xF,
                  Random() & 0xF);
        break;
    default:
        wnsprintf(buf, ARRAY_SIZE(buf), bsod4, bcc.code, Random() | 1 << 31, Random() | 1 << 31,
                  Random() | 1 << 31, Random() | 1 << 31);
        break;
    }
    lstrcat(bsod, buf);

We generate a temporary string for the STOP: line from bsod4 with wnsprintf (since we can’t use sprintf for aforementioned reasons), and then append it. There are four different ways the bug check arguments could be populated, since they aren’t always random numbers. If I cared more, I probably could have made the numbers more realistic based on the error codes, but right now, this just ensures that there are some small numbers, some big numbers, and some zeroes in the parameters. We also guarantee the high bit is set on the first parameter, which makes it look like a memory address in the upper half of the address space, commonly used for kernel mode addresses.

    lstrcat(bsod, bsod5);
    dwAddress = Random() | 1 << 31;
    wnsprintf(buf, ARRAY_SIZE(buf), bsod6, lpName, dwAddress, dwAddress & 0xFFFF0000, UnixTime());
    lstrcat(bsod, buf);

Finally, we add the last two lines, including the module name again and some addresses. We just generate a random faulting address, and set the module base to the start of the 64 kiB block, and then plug in the Unix time.

At this point, bsod contains all the text to be shown.

    SelectObject(hdc, hbmp);
    SelectObject(hdc, hFont);
    FillRect(hdc, &rect, hBrush);
    SetBkColor(hdc, RGB(0, 0, 128));
    SetTextColor(hdc, RGB(255, 255, 255));
    DrawText(hdc, bsod, -1, &rect, 0);

We now load the bitmap and font into the HDC, then fill the entire bitmap with the blue background brush. We then set the background and text colours, before drawing the text.

    DeleteDC(hdc);
    DeleteObject(hBrush);
    return hbmp;

We finally clean up the objects we’ve created, then return the bitmap created.

Entry point

Now we come to the startup sequence:

DWORD APIENTRY RawEntryPoint() {
    MSG messages;
    WNDCLASSEX wincl;
    HMODULE user32;

We declare stack variables for processing window messages and a window class to be registered, then an HMODULE to get a handle to user32.dll, whence we will load ShutdownBlockReasonCreate and ShutdownBlockReasonDestroy for newer versions of Windows.

    hInst = (HINSTANCE) GetModuleHandle(NULL);
    GenerateUUID(szDeskName);

    ProtectProcess();

We initialize hInst. In 32- and 64-bit Windows, the HINSTANCE is just the base address of the executable, which we obtain as above.

We then initialize szDeskName to be the secure desktop’s unique name, and protect the process from being killed.

    // Save the current sticky/toggle/filter key settings so they can be
    // restored them later
    SystemParametersInfo(SPI_GETSTICKYKEYS, sizeof(STICKYKEYS), &StartupStickyKeys, 0);
    SystemParametersInfo(SPI_GETTOGGLEKEYS, sizeof(TOGGLEKEYS), &StartupToggleKeys, 0);
    SystemParametersInfo(SPI_GETFILTERKEYS, sizeof(FILTERKEYS), &StartupFilterKeys, 0);

We save the initial state for the accessibility features too.

    hOldDesk = GetThreadDesktop(GetCurrentThreadId());
    hNewDesk = CreateDesktop(szDeskName, NULL, NULL, 0, GENERIC_ALL, NULL);
    SetThreadDesktop(hNewDesk);
    SwitchDesktop(hNewDesk);

We get the current desktop and then create a new one, then set the current thread to use that desktop, before switching the screen over to the new desktop.

If you have used Windows Vista or newer, you may remember the UAC prompt that dims the entire screen and heard it vaguely referred to as a “secure desktop”. This basically does exactly the same thing: we create a new desktop and switch to it. Since most normal GUI objects are desktop-scoped, including things like windows, menus, and most importantly, hooks, creating a new desktop isolates the window on it. In case of the secure desktop for UAC, no weird messages can be sent to windows on the new desktop, e.g. to simulate the “yes” button being clicked, and low-level keyboard hooks (which is how most keyloggers work) can’t be used to intercept the user’s keystrokes. This is how we defeat keyloggers.

The dimmed desktop for the UAC prompt is actually just a screenshot of the old desktop, dimmed, and rendered on a full screen window. Here, we don’t bother.

As a bonus, since no explorer is running on the secure desktop, it’s extra hard to find a way to break out of the BSoD.

#ifdef NOTASKMGR
    if (RegCreateKeyEx(HKEY_CURRENT_USER,
                       "Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System", 0, NULL, 0,
                       KEY_SET_VALUE, NULL, &hSystemPolicy, NULL)) {
        hSystemPolicy = NULL;
    }
#endif

If we are disabling the task manager, we need to load up hSystemPolicy. We open the registry key named, creating it if it doesn’t exist. If we should somehow fail, we set hSystemPolicy to NULL, in which case DisableTaskManager will fail gracefully.

    wincl.hInstance = hInst;
    wincl.lpszClassName = szClassName;
    wincl.lpfnWndProc = WndProc;
    wincl.style = CS_DBLCLKS;
    wincl.cbSize = sizeof(WNDCLASSEX);

    wincl.hIcon = LoadIcon(NULL, MAKEINTRESOURCE(1));
    wincl.hIconSm = LoadIcon(NULL, MAKEINTRESOURCE(1));
    wincl.hCursor = NULL;
    wincl.lpszMenuName = NULL;
    wincl.cbClsExtra = 0;
    wincl.cbWndExtra = 0;
    wincl.hbrBackground = (HBRUSH) GetStockObject(BLACK_BRUSH);

    if (!RegisterClassEx(&wincl))
        return 0;

We now populate the window class before registering this. This is just boilerplate, except that we set the background as BLACK_BRUSH to simulate the flash of black while the display mode changes, before the BSoD shows up.

    user32 = GetModuleHandle("user32");
    fShutdownBlockReasonCreate =
        (LPFN_SHUTDOWNBLOCKREASONCREATE) GetProcAddress(user32, "ShutdownBlockReasonCreate");
    fShutdownBlockReasonDestroy =
        (LPFN_SHUTDOWNBLOCKREASONDESTROY) GetProcAddress(user32, "ShutdownBlockReasonDestroy");

We get an HMODULE to user32.dll and then load up ShutdownBlockReasonCreate and ShutdownBlockReasonDestroy.

    hhkKeyboard = SetWindowsHookEx(WH_KEYBOARD_LL, LowLevelKeyboardProc, hInst, 0);
    hhkMouse = SetWindowsHookEx(WH_MOUSE_LL, LowLevelMouseProc, hInst, 0);

We also create low-level keyboard and mouse hooks to disable certain keys and all mouse movements.

    hwnd = CreateWindowEx(0, szClassName, "Blue Screen of Death", WS_POPUP, CW_USEDEFAULT,
                          CW_USEDEFAULT, 640, 480, NULL, NULL, hInst, NULL);

    ShowWindow(hwnd, SW_MAXIMIZE);

We create the window, and show it maximized.

    hAccel = CreateAcceleratorTable(accel, ARRAY_SIZE(accel));
    while (GetMessage(&messages, NULL, 0, 0) > 0) {
        if (!TranslateAccelerator(hwnd, hAccel, &messages)) {
            TranslateMessage(&messages);
            DispatchMessage(&messages);
        }
    }

We now run the classic Window loop, except we construct the accelerator table ahead of time and call TranslateAccelerator to interpret the keyboard accelerators. If that function returns true, then the message should not be processed further, per documentation, so we skip TranslateMessage and DispatchMessage in that case.

    ExitProcess(messages.wParam);
}

We close out RawEntryPoint with an ExitProcess call, since Windows by default passes the return value to ExitThread and leaves all other threads in the process running. Since threads may start for whatever reason these days, the process might linger on indefinitely, and we ensure this isn’t the case via ExitProcess.

The exit code is the wParam of the last message processed. The only way the message loop can terminate is if it receives WM_QUIT, which is generated by the PostQuitMessage function. Effectively, we exit the program with the value passed to PostQuitMessage.

Low-level mouse hook

This is the low-level mouse hook, which just swallows all mouse events:

LRESULT CALLBACK LowLevelMouseProc(int nCode, WPARAM wParam, LPARAM lParam) {
    if (nCode >= 0) {
        return 1;
    }
    return CallNextHookEx(NULL, nCode, wParam, lParam);
}

Low-level keyboard hook

This is the low-level keyboard hook, which swallows all keys not used in any of our accelerators or would be required in the password dialogue:

LRESULT CALLBACK LowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) {
    if (nCode >= 0) {
        KBDLLHOOKSTRUCT *key = (KBDLLHOOKSTRUCT *) lParam;

        switch (key->vkCode) {
        case VK_LWIN:
        case VK_RWIN:
        case VK_TAB:
        case VK_ESCAPE:
        case VK_LBUTTON:
        case VK_RBUTTON:
        case VK_CANCEL:
        case VK_MBUTTON:
        case VK_CLEAR:
        case VK_PAUSE:
        case VK_CAPITAL:
        case VK_KANA:
        case VK_JUNJA:
        case VK_FINAL:
        case VK_HANJA:
        case VK_NONCONVERT:
        case VK_MODECHANGE:
        case VK_ACCEPT:
        case VK_END:
        case VK_HOME:
        case VK_LEFT:
        case VK_UP:
        case VK_RIGHT:
        case VK_DOWN:
        case VK_SELECT:
        case VK_PRINT:
        case VK_EXECUTE:
        case VK_SNAPSHOT:
        case VK_INSERT:
        case VK_HELP:
        case VK_APPS:
        case VK_SLEEP:
        case VK_NUMLOCK:
        case VK_SCROLL:
        case VK_PROCESSKEY:
        case VK_PACKET:
        case VK_ATTN:
            return 1;
        }
    }
    return CallNextHookEx(NULL, nCode, wParam, lParam);
}

Window procedure

The window procedure is the event handler for the window, effectively. It starts like this:

LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) {
    int i;
    switch (message) {

We define a loop variable for use later due to C89 and use a switch statement to match on the message ID.

    case WM_CREATE:
        scwnd = CreateWindowEx(0, "STATIC", "", SS_BITMAP | WS_CHILD | WS_VISIBLE, 0, 0, 640, 480,
                               hwnd, (HMENU) -1, NULL, NULL);
#ifndef NOAUTOKILL
        SetTimer(hwnd, TM_AUTOKILL, AUTOKILL_TIMEOUT, NULL);
#endif
        SetTimer(hwnd, TM_DISPLAY, DISPLAY_DELAY, NULL);
        SetTimer(hwnd, TM_FORCEDESK, FORCE_INTERVAL, NULL);

        SetCursor(NULL);

        if (fShutdownBlockReasonCreate)
            fShutdownBlockReasonCreate(hwnd, L"You can't shutdown with a BSoD running.");

        // Force to front
        SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0,
                     SWP_ASYNCWINDOWPOS | SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE);
        SetForegroundWindow(hwnd);
        LockSetForegroundWindow(1);

        AllowAccessibilityShortcutKeys(FALSE);
#ifdef NOTASKMGR
        DisableTaskManager();
#endif
        break;

In our WM_CREATE handler, which is called upon window creation, we create a static control to display our blue screen bitmap.

We also set a bunch of timers:

  1. If NOAUTOKILL is not defined, then we set up a timer to signal the program to exit, which is invaluable for debugging;
  2. We set a timer to display the BSoD after initially blacking out the screen; and
  3. We set a timer to forcefully switch the desktop to our secure desktop, in case someone switches it back somehow.

We also hide the cursor, since a mouse cursor on the BSoD gives the game away pretty quickly.

We then create a shutdown blocking reason. Note that fShutdownBlockReasonCreate takes exclusively Unicode strings, so we needed to use a const wchar_t * literal with the L prefix on the string.

We then move the window to the front through SetWindowPos, with HWND_TOPMOST to guarantee the window shows up above all normal windows. We set it to the foreground window and prevent any other process from taking over with LockSetForegroundWindow.

Finally, we disable accessibility shortcuts and the task manager.

    case WM_SHOWWINDOW:
        return 0;

We ignore all WM_SHOWWINDOW by returning 0 immediately, skipping the processing in DefWindowProc, which may hide the current window.

    case WM_TIMER:
        switch (wParam) {

We now handle the timers.

        case TM_DISPLAY: {
            RECT rectClient;

            KillTimer(hwnd, TM_DISPLAY);
            SendMessage(scwnd, STM_SETIMAGE, (WPARAM) IMAGE_BITMAP, (LPARAM) RenderBSoD());
            GetClientRect(hwnd, &rectClient);
            SetWindowPos(scwnd, 0, 0, 0, rectClient.right, rectClient.bottom,
                         SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
            InvalidateRect(hwnd, NULL, TRUE);
            break;
        }

For TM_DISPLAY, we stop the timer from firing again, then render the BSoD and set it as the bitmap shown by the static control. We then make the static control cover the whole screen, before forcing the whole window to repaint.

#ifndef NOAUTOKILL
        case TM_AUTOKILL:
            KillTimer(hwnd, TM_AUTOKILL);
            DestroyWindow(hwnd);
            break;
#endif

If the safety timeout triggers, we stop the timer from triggering again, and then call DestroyWindow to tear everything down.

        case TM_FORCEDESK:
            SwitchDesktop(hNewDesk);
            break;
        }
        break;

This is pretty self-explanatory.

    case WM_DESTROY:
        if (fShutdownBlockReasonDestroy)
            fShutdownBlockReasonDestroy(hwnd);

        UnhookWindowsHookEx(hhkKeyboard);
        UnhookWindowsHookEx(hhkMouse);

        DestroyAcceleratorTable(hAccel);
        LockSetForegroundWindow(0);
        AllowAccessibilityShortcutKeys(TRUE);
        SetThreadDesktop(hOldDesk);
        SwitchDesktop(hOldDesk);
        CloseDesktop(hNewDesk);
#ifdef NOTASKMGR
        EnableTaskManager();
#endif
        PostQuitMessage(0);
        break;

Upon receiving WM_DESTROY, we perform cleanup:

  • removing the shutdown block on newer Windows;
  • unhook the mouse and keyboard;
  • clean up the acclerator table;
  • unlock the foreground window;
  • reset the accessibility shortcuts;
  • switch the desktop back;
  • clean up the secure desktop;
  • re-enable the task manager if we disabled it; and
  • exit the message loop and the program.
    case WM_CLOSE:
        switch (DialogBox(hInst, MAKEINTRESOURCE(32), hwnd, DlgProc)) {
        case 1:
            DestroyWindow(hwnd);
            break;
        case 2:
            hdlg = HDLG_MSGBOX;
            MessageBox(hwnd,
                       "You got the password wrong!\n"
                       "Good luck guessing!",
                       "Error!", MB_ICONERROR);
            hdlg = NULL;
            break;
        default:
            hdlg = HDLG_MSGBOX;
            MessageBox(hwnd,
                       "You just abandoned the perfect chance to exit!\n"
                       "Good luck trying!",
                       "Error!", MB_ICONERROR);
            hdlg = NULL;
        }
        break;

Upon receiving WM_CLOSE, which means someone is trying to close the window, we invoke DialogBox to create a dialogue from our resource template and use DlgProc as the dialogue procedure.

If the dialogue procedure returns 1, signalling the password is correct, we destroy the window. If it returns 2, we show a taunting message for an incorrect password, and any other value means the dialogue box was closed, and we show a different taunting message.

For reference, the dialogue template is in bsod.rc and looks like:

#include <winuser.h>

#define IDC_EDIT1  1024
#define IDC_STATIC -1

32 DIALOGEX 0, 0, 256, 73
STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "Dialog"
FONT 8, "MS Shell Dlg", 400, 0, 0x1
BEGIN
    EDITTEXT        IDC_EDIT1,47,33,201,14,ES_AUTOHSCROLL | ES_PASSWORD
    LTEXT           "It seems like you are trying to exit a Blue Screen of Death, which is impossible. But if you are a hacker and knows the password, you might be able to.",IDC_STATIC,7,7,240,27
    LTEXT           "Password:",IDC_STATIC,7,36,38,8,0,WS_EX_TRANSPARENT
    PUSHBUTTON      "&OK",IDOK,145,49,50,14
    PUSHBUTTON      "&Cancel",IDCANCEL,198,49,50,14
END

Back to the window procedure…

    case WM_KEYDOWN:
        return 0;

We skip the weird WM_KEYDOWN handling for F10 in DefWindowProc by doing this.

    case WM_COMMAND:
    case WM_SYSCOMMAND:
        if (HIWORD(wParam) == 1) {
            if (LOWORD(wParam) == 0xDEAD) {
                for (i = 0; i < ARRAY_SIZE(bAccel); ++i)
                    if (!bAccel[i])
                        return 0;

                SendMessage(hwnd, WM_CLOSE, 0, 0);
            } else if ((LOWORD(wParam) & 0xFF00) == 0xBE00) {
                int index = LOWORD(wParam) & 0xFF;
                if (index < ARRAY_SIZE(bAccel))
                    bAccel[index] = TRUE;
            }
        }
        break;

This handles the keyboard accelerators. Whether WM_COMMAND or WM_SYSCOMMAND is generated is kinda complex and it doesn’t really matter here, so we just treat them the same. If HIWORD(wParam) == 1, then it’s an accelerator key, and LOWORD(wParam) is what we put into the accelerator table above:

  • If it’s 0xDEAD and all other accelerators have been pressed, we send WM_CLOSE, initiating the procedure above to show the password prompt.
  • If it’s 0xBExx and the low-order byte is a valid index, we mark it as pressed.
    case WM_QUERYENDSESSION:
        return 0;

This tells Windows XP that the user shouldn’t be allowed to log out. Typically, this is done in response to unsaved documents, but we are abusing it to prevent logouts without the password.

Perhaps due to applications just returning 0 for all messages instead of calling DefWindowProc, or otherwise abusing WM_QUERYENDSESSION, Windows Vista introduced the whole ShutdownBlockReasonCreate thing…

    case WM_ENFORCE_FOCUS:
        if (GetForegroundWindow() != hdlg) {
            if (hdlg && hdlg != HDLG_MSGBOX) {
                SetFocus(hdlg);
                SetForegroundWindow(hdlg);
            } else if (!hdlg) {
                SetFocus(hwnd);
                SetForegroundWindow(hwnd);
                SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
            }
        }
        SetCursor(NULL);
        ShowWindow(hwnd, SW_SHOWMAXIMIZED);
        break;

This is a custom window message that we use to reset the foreground window, should we somehow stop being it. We set the focus on hdlg if it exists, otherwise the main window, which we ensure is topmost. However, if we are showing a message box, we don’t change the focus.

We also hide the cursor and maximize the window.

    case WM_ACTIVATE:
        if (LOWORD(wParam) != WA_INACTIVE)
            break;
        if (!HIWORD(wParam))
            break;

    case WM_NCACTIVATE:
    case WM_KILLFOCUS:
        PostMessage(hwnd, WM_ENFORCE_FOCUS, 0, 0);
        return 1;

If we detect the window has been deactivated somehow or lost focus, we post the message WM_ENFORCE_FOCUS to be handled the next time the message loop runs.

    case WM_SIZE:
        if (wParam != SIZE_MAXIMIZED)
            ShowWindow(hwnd, SW_SHOWMAXIMIZED);
        break;

If we receive a WM_SIZE telling us we are no longer maximized, undo that.

    default:
        return DefWindowProc(hwnd, message, wParam, lParam);
    }
    return 0;
}

And we wrap up the window procedure by calling DefWindowProc for all other messages, and return 0 if we’ve handled any other message that didn’t return early.

Dialogue procedure

And finally, we have a dialogue procedure for the password dialogue:

INT_PTR CALLBACK DlgProc(HWND hWndDlg, UINT msg, WPARAM wParam, LPARAM lParam) {
    UNREFERENCED_PARAMETER(lParam);

    switch (msg) {

This is how it starts. We use the UNREFERENCED_PARAMETER macro to avoid a warning being generated by Microsoft’s compilers at /W4 level of warnings, since we never touch lParam.

We then handle each dialogue message, which acts like window messages…

    case WM_INITDIALOG:
        hdlg = hWndDlg;
        break;

Upon WM_INITDIALOG, which is sent when the dialogue is initialized, we set hdlg to save the window handle.

    case WM_COMMAND:
        switch (wParam) {

Upon WM_COMMAND, which happens when a button is clicked, we switch on wParam, which contains the ID of the button being clicked.

        case IDOK: {
            DWORD dwLength, i;
            TCHAR szPassword[PASSWORD_LENGTH + 1];
            dwLength = GetDlgItemText(hWndDlg, IDC_EDIT1, szPassword, PASSWORD_LENGTH + 1);
            if (dwLength != PASSWORD_LENGTH) {
                EndDialog(hWndDlg, 2);
                hdlg = NULL;
                break;
            }

            for (i = 0; i < PASSWORD_LENGTH; ++i)
                szPassword[i] ^= 37;

            EndDialog(hWndDlg, lstrcmp(szPassword, szRealPassword) ? 2 : 1);
            hdlg = NULL;
            break;

IDOK is the default ID for the OK button in a dialogue box, and we use GetDlgItemText to get the text for the dialogue item with IDC_EDIT1, which is our password edit control. We load the bytes into szPassword, which is just enough to fit the correct password.

If GetDlgItemText returns the wrong length, we end the dialogue with code 2, meaning incorrect password.

Then, we XOR every byte in the password with 37 to encode it, before using lstrcmp to compare it with the real password. lstrcmp is kernel32.dll’s version of strcmp, which again we can’t use due to shunning the C library.

We end the dialogue with code 1 if it’s correct, otherwise 2.

        case IDCANCEL:
            EndDialog(hWndDlg, 0);
            hdlg = NULL;
            break;
        }
        break;

We end the dialogue with 0 otherwise, to signal the dialogue was cancelled.

    default:
        return FALSE;
    }
    return TRUE;
}

And finally, we return TRUE if we handled the message, and FALSE if we didn’t to trigger the default behaviour. This is where dialogue procedures differ from window ones, and honestly, I think the DefWindowProc approach is more flexible.

Conclusion

And that’s the end of this little program. I hope you learned a bit about programming in C or writing Windows applications, especially a funky one like this.

Notes

  1. Yes, this predated the Windows XP end-of-support date of April 8, 2014, but the school just continued running Windows XP anyway. It wasn’t until 2015 that all the computers were upgraded to Windows 7, and boy was it slow on those ancient Pentium Ds, which were around 10 years old by that time. It probably explained why they held onto Windows XP as long as possible.

    For those who are too young to remember, that was a reasonably innovative era in PC hardware, so 10-year-old computers were a lot more unusable compared to 2026. 

  2. I obviously knew which computer was the fastest in every lab and made sure to secure it for my own use whenever possible. They were still super slow, especially in the Windows 7 era, but better than being stuck with something even worse. 

  3. Yes, Microsoft used Unix time on the BSoD. It is perhaps somewhat ironic, but they did make Xenix at one point… 

  4. Yes, it was an ancient toolchain, surprisingly lightweight and popular, even a decade and half after its release. Pretty good for a toolchain as old as me. 

  5. For those of you wondering how I got VC6 to run on Jenkins, the answer is Wine, because I am not specifically deploying a Windows server to run VC6. My debloated VC6 package just ran perfectly fine. 

  6. For those too young to remember, AMD’s graphics card division was acquired from ATI.