Blog Post

Things You Thought You Knew - Getting Windows Version

How to tell the "real" version of Windows your app is running on?

Things You Thought You Knew - Getting Windows Version - How to tell the "real" version of Windows your app is running on?
Image courtesy of Olga Barinova
This article contains functions and features that are not documented by the original manufacturer. By following advice in this article, you're doing so at your own risk. The methods presented in this article may rely on internal implementation and may not work in the future.

Intro

There are many reasons why a developer would need to know the version of the operating system that their app is running on. But, since about the time of Windows 8.1, Microsoft has decided that developers are not supposed to know the version of Windows that their software runs on, and deprecated the APIs that were previously used for it, such as GetVersion, GetVersionEx, VerifyVersionInfo and others. So now those APIs simply lie about the version of the OS and are pretty much useless.

This game of cat-and-mouse, that Microsoft plays with their developers about determining the version of the operating system, really puzzles me. Especially when there are so many ways to circumvent their restrictions and get a true version of the OS by using many hacks, such as: a version number from some system DLL, a presence of a certain API, or even by scanning the system registry. Unfortunately though, this silly game only introduces complexity and propensity for a buggy software.

If you ask about this online, I'm sure many people on some popular programming forums would find a reason to ask you back, "Why do you need to know this", instead of answering the question. So I won't follow that annoying practice, and show you how to do it without wasting too much of your time.

There are two ways how you can get the OS version. Let's review them both.

Getting Version Numbers

If you want to restrict or add certain functions to your app, depending on which OS it runs on, the easiest way to make that decision is by getting the version numbers of the operating system.

An interesting side note worth to mention here, is that Microsoft plays the faux-version game with user-mode developers only. For the kernel mode it is quite critical to know which version of the OS your driver runs on, and thus, Microsoft is not engaging in the same shenanigans with the kernel developers.

Thus, by using the native APIs, which are a carbon copy of their kernel counterparts, you can circumvent the user-mode "true version" restriction.

The API to use to get the real OS version numbers is an undocumented RtlGetNtVersionNumbers. It is exported from ntdll.dll and was available as far back as Windows XP. It is declared as such:

C++
void WINAPI RtlGetNtVersionNumbers(
    __out_opt ULONG* pNtMajorVersion,
    __out_opt ULONG* pNtMinorVersion,
    __out_opt ULONG* pNtBuildNumber
);

All of its input parameters are optional, and you can pass a NULL if you don't need some value.

After that you can use this chart to determine which operating system your app is running on, using the version numbers returned by that function.

Note that the last parameter of the RtlGetNtVersionNumbers function, receives a bitmask of values. The upper 4 bits of it are reserved for the type of the OS build:

  • 0xC for a "checked" (or debug) build, and
  • 0xF for a "free" (or production/retail) build.

Thus, if you want to get the build number from the 3rd parameter, you need to clear out its highest 4 bits.

The RtlGetNtVersionNumbers function can be used by statically linking to ntdll.dll as such:

C++
#include <iostream>
#include <windows.h>

#pragma comment(lib, "ntdll.lib")

extern "C" {
__declspec(dllimport) void WINAPI RtlGetNtVersionNumbers(
	__out_opt ULONG* pNtMajorVersion,
	__out_opt ULONG* pNtMinorVersion,
	__out_opt ULONG* pNtBuildNumber
	);
};


int main()
{
	ULONG MajorVersion = 0;
	ULONG MinorVersion = 0;
	ULONG BuildNumber = 0;

	RtlGetNtVersionNumbers(&MajorVersion, &MinorVersion, &BuildNumber);

	//Extract the build number
	std::cout << "Windows v." << MajorVersion << "." << MinorVersion << 
		" build: " << (BuildNumber & ~0xF0000000) << std::endl;

	//Is it a "checked" build?
	switch(BuildNumber & 0xF0000000)
	{
	case 0xC << (7 * 4):
		std::cout << "Debugging build" << std::endl;
		break;
	case 0xF << (7 * 4):
		std::cout << "Production build" << std::endl;
		break;
	}

	return 0;
}

Or, you can dynamically link to it during runtime using the GetProcAddress API if you don't like using ntdll.lib for static linking:

C++
ULONG MajorVersion = 0;
ULONG MinorVersion = 0;
ULONG BuildNumber = 0;

void (WINAPI *pfnRtlGetNtVersionNumbers)(
	__out_opt ULONG* pNtMajorVersion,
	__out_opt ULONG* pNtMinorVersion,
	__out_opt ULONG* pNtBuildNumber
);

(FARPROC&)pfnRtlGetNtVersionNumbers = 
	GetProcAddress(GetModuleHandle(L"ntdll.dll"), "RtlGetNtVersionNumbers");

if(pfnRtlGetNtVersionNumbers)
{
	pfnRtlGetNtVersionNumbers(&MajorVersion, &MinorVersion, &BuildNumber);
}
else
{
	assert(false);
}

The output from RtlGetNtVersionNumbers on my old Windows XP machine is:

Windows v.5.1 build: 2600 Production build

On my Windows 10:

Windows v.10.0 build: 19044 Production build

And on Windows 11, it is:

Windows v.10.0 build: 25211 Production build
Note, that Microsoft doesn't make things easy with their internal version numbering. One would assume that Windows 11 would have a major version number set as 11. But this doesn't seem to be the case. They still consider Windows 11 to have a major version number of 10. So, to distinguish between Windows 10 and 11 you need to rely on the BuildNumber. Anything greater or equal to 20000 will indicate Windows 11. Anything from [10000 to 20000), Windows 10.

Alternatively, you can use the following approach if you need to get a text representation of the OS version.

Getting Version As a String

Quite often though you may need to display the version of the operating system using a human-readable string. For instance, if you are implementing a bug reporting capability in your app and need to fill out the version of the OS in your report. Thus it may be easier to use a different internal API instead of parsing the integer output from the RtlGetNtVersionNumbers function, which can get quite complicated.

There are many publicly available methods of extracting a string name of the operating system. Unfortunately most of them are quire flawed in that they do not always work over time with new releases of Windows.

To remedy the situation I decided to see how Microsoft are doing it themselves. Thus I reverse-engineered their winver.exe app to see the details of their implementation:

Windows 11 version
winver command executed on Windows 11 Insider Preview.

It turns out that their approach is quite simple. They use an undocumented module winbrand.dll that, among others, exports the BrandingFormatString function. It is declared as such:

C++
PWSTR (WINAPI* BrandingFormatString)(__in PCWSTR pstrFormat);
This function was available for a long time, since Windows Vista.

On the input it receives a string that may contain formatting specifiers (akin to printf).

The return parameter is a string with the specifiers replaced with their actual values. The return value must be freed with a call to GlobalFree to avoid memory leaks. This function always seems to return a non-0 value, but it may be prudent to check it for a NULL.

The formatting specifiers available for this function are the following on my Windows 10 machine:

%WINDOWS_GENERIC%
%WINDOWS_SHORT%
%WINDOWS_LONG%
%WINDOWS_PRODUCT%
%WINDOWS_COPYRIGHT%
%MICROSOFT_COMPANYNAME%
%MICROSOFT_ACCOUNT%
%MICROSOFT_ACCOUNTS%
%WINDOWS_CLIENT_VERSION_6_1%
%WINDOWS_SERVER_VERSION_6_1%
%WINDOWS_CLIENT_VERSION_6_2%
%WINDOWS_SERVER_VERSION_6_2%
%WINDOWS_ARM_VERSION_6_2%
%WINDOWS_CLIENT_VERSION_6_3%
%WINDOWS_SERVER_VERSION_6_3%
%WINDOWS_ARM_VERSION_6_3%
%WINDOWS_CLIENT_VERSION_6_4%
%WINDOWS_SERVER_VERSION_6_4%
%WINDOWS_ARM_VERSION_6_4%
%WINDOWS_CLIENT_VERSION_10_0%
%WINDOWS_SERVER_VERSION_10_0%
%WINDOWS_SERVER_VERSION_10_0_2019_LTSC%
%IDS_WINDOWS_SERVER_VERSION_10_0_1809_SAC%
I'm sure I don't need to tell you that there's no guarantee that these will be preserved in the future versions of Windows, or that they were present in the old versions. The ones on the top seem to be present in the old versions of the OS as well.

The output from these specifiers may be a localized string, depending on the language installed in the operating system.

Specifier Examples

Below is an example of the specifiers and their values on different (English) Windows systems that I observed:

Specifier Windows Vista Windows 10 Windows 11 (Insider)
%WINDOWS_GENERIC% "Windows" "Windows" "Windows"
%WINDOWS_SHORT% "Windows Vista" "Windows 10" "Windows 10"
%WINDOWS_LONG% "Windows Vista Ultimate" "Windows 10 Pro" "Windows 11 Pro Insider Preview"
%WINDOWS_PRODUCT% "Ultimate" "Windows 10 Pro" "Windows 11 Pro Insider Preview"
%WINDOWS_COPYRIGHT% "Copyright © 2007 Microsoft Corporation. All rights reserved." "© Microsoft Corporation. All rights reserved." "© Microsoft Corporation. All rights reserved."
%MICROSOFT_COMPANYNAME% "Microsoft" "Microsoft" "Microsoft"
%MICROSOFT_ACCOUNT% - "Microsoft account" "Microsoft account"
%MICROSOFT_ACCOUNTS% - "Microsoft accounts" "Microsoft accounts"
%WINDOWS_CLIENT_VERSION_6_1% - "Windows 7" "Windows 7"
%WINDOWS_SERVER_VERSION_6_1% - "Windows Server 2008 R2" "Windows Server 2008 R2"
%WINDOWS_CLIENT_VERSION_6_2% - "Windows 8" "Windows 8"
%WINDOWS_SERVER_VERSION_6_2% - "Windows Server 2012" "Windows Server 2012"
%WINDOWS_ARM_VERSION_6_2% - "Windows RT" "Windows RT"
%WINDOWS_CLIENT_VERSION_6_3% - "Windows 8.1" "Windows 8.1"
%WINDOWS_SERVER_VERSION_6_3% - "Windows Server 2012 R2" "Windows Server 2012 R2"
%WINDOWS_ARM_VERSION_6_3% - "Windows RT 8.1" "Windows RT 8.1"
%WINDOWS_CLIENT_VERSION_6_4% - "Windows Technical Preview" "Windows Technical Preview"
%WINDOWS_SERVER_VERSION_6_4% - "Windows Server Technical Preview" "Windows Server Technical Preview"
%WINDOWS_ARM_VERSION_6_4% - "Windows Technical Preview" "Windows Technical Preview"
%WINDOWS_CLIENT_VERSION_10_0% - "Windows 10" "Windows 10"
%WINDOWS_SERVER_VERSION_10_0% - "Windows Server 2016" "Windows Server 2016"
%WINDOWS_SERVER_VERSION_10_0_2019_LTSC% - "Windows Server 2019" "Windows Server 2019"
%IDS_WINDOWS_SERVER_VERSION_10_0_1809_SAC% - "Windows Server" "Windows Server"

The most interesting specifiers for our case are: %WINDOWS_LONG% and %WINDOWS_PRODUCT%.

Note the difference between the values of %WINDOWS_SHORT% and %WINDOWS_LONG% on Windows 10 and Windows 11. This doesn't help with the confusion, does it?

There's no .lib file for the winbrand.dll module available in the Windows SDK. And even Microsoft seem to be loading it during runtime in their winver.exe app. You can do it as such:

C++
HMODULE hMod = LoadLibraryEx(L"winbrand.dll", NULL, LOAD_LIBRARY_SEARCH_SYSTEM32);
if(hMod)
{
	PWSTR (WINAPI* pfnBrandingFormatString)(PCWSTR pstrFormat);

	(FARPROC&)pfnBrandingFormatString = 
		GetProcAddress(hMod, "BrandingFormatString");

	if(pfnBrandingFormatString)
	{
		PWSTR pstrOSName = pfnBrandingFormatString(
			L"The operating system is: %WINDOWS_LONG%");

		wprintf(L"%s\n", pstrOSName);

		//Remember to free the memory!
		GlobalFree((HGLOBAL)pstrOSName);
	}
	else
		assert(false);

	FreeLibrary(hMod);
}
else
	assert(false);

The code above outputs the following in my Windows 10 machine:

The operating system is: Windows 10 Pro

Which can be used for a debugging output in your event log file.

Conclusion

The approaches that I showed above have been used quite extensively by Microsoft themselves for quite some time now. But due to their undocumented nature the functions involved can be altered in the future without notice. So, if you use them, make sure to put necessary precautions and fallbacks to account for possibility of a change.

Related Articles