Making nice UI on NT 3.1 and above

13 Sep 2023

As somebody who still writes software for NT 3.1, one recurring challenge is how to ensure any GUI code can function on NT 3.1 while looking decent on anything newer. NT 3.1 won't run anything with a subsystem newer than 3.10; but a subsystem of 3.10 tells newer systems to make them look ugly.

A horrifying solution occurred to me: what if the Windows UI code thinks the binary is targeting a newer OS? The binary actually has to target an older one, but so long as the UI code thinks it targets a newer one, we're good.

After a bit of investigation, I found that User32.dll inspects the subsystem version, including as part of loading User32.dll. Normally programs would link User32.dll into their import table, so it would be loaded immediately on process load. But it's possible to load dynamically, allowing the program to run its own code first, before User32 can initialize.

Unfortunately the subsystem field is part of the memory mapping of the binary in memory, so modifying it means firstly making the page writable, then modifying it, restoring page permissions, then loading User32.

With that, here's the same binary. First, on NT 3.1:

Then, NT 4.0:

Programs written for NT 4.0 still look generally decent today, so this is all that's needed for a single binary to work well everywhere.

Here's the code that made it possible:

    //
    //  Give ourselves write access to the PE header, change it, and restore
    //  permissions.
    //

    if (!VirtualProtect(DosHeader, PageSize, PAGE_READWRITE, &OldProtect)) {
        return FALSE;
    }

    PeHeaders->OptionalHeader.MajorSubsystemVersion = NewMajor;
    PeHeaders->OptionalHeader.MinorSubsystemVersion = NewMinor;

    VirtualProtect(DosHeader, PageSize, OldProtect, &OldProtect);

The frustrating part about doing this is every User32 function needs to be dynamically loaded to ensure that none of it is loaded before the code above runs. So it seems painful to do for a complex application, but it's at least feasible for smaller programs.