Sunday, August 5, 2012

Major / MinorSubsystemVersion

If you are still using Windows 2000, you must have noticed that certain executables refuse to run. Actually, this is due to the executables being built with Microsoft Visual Studio 2010 which sets the MajorSubSystemVersion and MinorSubsystemVersion in the PE header to 5 and 1. In other words, it creates executables to run on Windows XP (5.1) and above. This causes Windows 2000 (5.0) to refuse to load these executables.

Now, let's see where the check occurs and how to bypass it. The first place to check must be the kernel32 "CreateProcess" function.

If we start at address 0x7C4F1ECE, we can see a call to the ntdll "ZwQuerySection" function with the "InformationClass" parameter set to 1 (SectionImageInformation). After the "ZwQuerySection" function has returned successfully, the "SECTION_IMAGE_INFORMATION" structure should be filled with some useful data. Among the data returned are the executable's subsystem type and minor and major versions.

Then comes a check for the subsystem type. The subsystem type must be either GUI (IMAGE_SUBSYSTEM_WINDOWS_GUI) or console (IMAGE_SUBSYSTEM_WINDOWS_CUI). If it is not any of these two types, the "CreateProcess" function fails.

As you can see in the image above, at address 0x7C4F1F91, the major and minor subsystem versions extracted from the PE header via the "ZwQuerySection" function are passed to the "CheckSubSystem" function. If the "CheckSubSystem" function returns TRUE, the "CreateProcess" function proceeds and if it returns FALSE, the "CreateProcess" function fails as such. Now, let's check this function.

As you can see in the disassembly and C-code in the three images above, if the subsystem versions extracted from the PE header are less than 3.10, the "CheckSubsystem" function returns FALSE. Then comes the important part, if the "MajorSubsystemVersion" extracted from the PE header is greater than the value of the "NtMajorVersion" field (The field is at offset 0x26C from the _KUSER_SHARED_DATA page), the function fails. The same applies for "MinorSubSystemVersion" if "MajorSubsystemVersion" and "NtMajorVersion" are equal.

N.B. NtMajorVersion and NtMinorVersion are usually the same as the OS version info. returned by the kernel32 "GetVersion" or "GetVersionEx" functions.

As a developer, bypassing the check can easily be done by using Platform Toolset v9 in microsoft visual studio (thanks @skier_t) or by directly editing the PE header of the executable using any PE Editor. 

Imagine the scenario where the executable in question has CRC check upon its PE header as part of the implemented protection scheme. In this case, as a user, you won't be able to run the executable since any attempt to edit the PE header will cause the CRC check to fail. This leads us to find a system-wide solution. Yes, patching.

Speaking of patching, we have two options:

1) The first is to patch a couple of addresses inside the "CheckSubSystem" function (Actually, i don't recommend patching the return value check).

To implement the check bypass, i created  a dynamic link library, hooksubsystem.dll that once injected into a process bypasses the subsystem version check.

You can find the source code of hooksubsystem.dll here.
You can find hooksubsystem.dll here.

One drawback of this method is that it is Service pack-specific since the "CheckSubSystem" function is not exported by kernel32.dll.

2) The second is to patch the "ZwQuerySection" function such that we can manipulate the data returned in the "SECTION_IMAGE_INFORMATION" structure before being used by "CheckSubSystem" function.

To implement this method, i created another version of hooksubsystem.dll. You can find it here and its source code from here.

I also created a small application, BypassSubSystem.exe, which installs a system-wide hook of the type provided in the command line arguments. It can be used in the way you see in the image below.
BypassSubSystem.exe can be downloaded from here and its source code from here.

In a future post i will go deeper into this topic. 

You can follow me on Twitter @waleedassar


  1. Well, that is an interesting way to address the VC10 (VS2010) issue of W2K incompatibility ;). Myself, I build using the VC9 build tools from within VS2010 to maintain backwards compatibility. This is a simple option in the Project Settings (Build Toolset=VC9).

    While adjusting the subsystem version on the PE modules might be adequate, I preferred a solution I could be sure of, as the CRT itself may have W2K incompatibilities. If it does, this system wide patch may have consequences. If it doesn't, or has few, then maybe it has no consequences.

    The larger question might be - does anyone need Windows 2000? In evaluation of this question, I have actually decided to drop W2K at some point, and there be a 'last working build'. I may update it from time to time, but Microsoft has made it harder and harder to maintain compatibility. Once they drop it, they strongly encourage software vendors to do so as well. I plan to do this in VS2012, so I can finally switch to the latest MSVC build tools, with their presumably improved optimizations over VC9.

  2. The links to the example files are broken!