Windows系统版本号判定那些事儿
前言
本文并非讨论Windows操作系统的版本号来历和特点,也不是讨论为什么没有Win9,而是从程序猿角度讨论下Windows获取系统版本号的方法和遇到的一些问题。在Win8和Win10出来之后,在获取系统版本号时,可能非常多人都碰到了相似的问题,为什么曾经工作得非常好的API,突然開始说谎了?
我们一般怎么获取系统版本号
我想用的最多的可能就是这两个API了吧。
DWORD WINAPI GetVersion (VOID);
BOOL WINAPI GetVersionExW(__inout LPOSVERSIONINFOW lpVersionInformation);
事实上GetVersion和GetVersionExW的实现是相似的,内部都是调用的NtCurrentPeb这个函数,另一个GetVersionExA内部则是调用的GetVersionExW来实现。
GetVersionExW大概是这么实现的(这仅仅是Windows2000的源代码,后面的新系统,OSVERSIONINFOW这个结构多了几倍的成员)。
WINBASEAPI BOOL WINAPI GetVersionExW(
LPOSVERSIONINFOW lpVersionInformation)
{
PPEB Peb;
if (lpVersionInformation->dwOSVersionInfoSize != sizeof( *lpVersionInformation )) {
SetLastError( ERROR_INSUFFICIENT_BUFFER );
return FALSE;
}
Peb = NtCurrentPeb();
lpVersionInformation->dwMajorVersion = Peb->OSMajorVersion;
lpVersionInformation->dwMinorVersion = Peb->OSMinorVersion;
lpVersionInformation->dwBuildNumber =Peb->OSBuildNumber;
lpVersionInformation->dwPlatformId = Peb->OSPlatformId;
wcscpy(lpVersionInformation->szCSDVersion,BaseCSDVersion );
return TRUE;
}
当中BaseCSDVersion是个全局变量,存放的是系统SP的字符串信息,在DLL初始化的时候就已经赋值了,由BaseDllInitialize来初始化。重点看下NtCurrentPeb这个函数,事实上非常显然,GetVersionExW就是从PEB里面去拷贝版本号信息。NtCurrentPeb是一个调用比較频繁的函数,它返回当前进程的PEB结构地址,也就是通过fs寄存器去定位PEB,然后在GetVersionExW里面把PEB里面的系统版本号信息拷贝给GetVersionExW的传出參数,也就是上面的OSMajorVersion等成员。
如今为什么不行了
可是从Windows8.1出来之后,GetVersionExW这个API被微软明文给废弃了,这个坑下得可够大的(參考[1])。也就是说从Windows8.1開始之后(包含Windows10),这个API常规情况下就是返回6.2了。
“In Windows 8.1, the GetVersion(Ex)APIs have been deprecated. That means that while you can still call the APIs,if your app does not specifically target Windows 8.1, you will getWindows 8 versioning (6.2.0.0).”
可是此时你去查看应用软件PEB的信息,发现PEB里面的系统版本号还是正确的,在Windows10以下调试了一下,发现可是GetVersionExW确实返回的是6.2,可是PEB里面的版本号则是6.4。也就是说微软更改了这个API的实现。
去调试微软对这个API做了什么改动意义不大,反正如今的结果就是这个API返回的值不正确了,API也開始说谎了~只是在[1]里面,微软同一时候给出一个解决方式,嗯,一边跟你说,这个API已经被废弃了,一边又说还是能够用的,这不是坑爹是什么……解决方式是什么呢?改动manifest文件。加一段compatibility节点。
<?xml version="1.0"encoding="UTF-8" standalone="yes"?>
<assembly manifestVersion="1.0"xmlns="urn:schemas-microsoft-com:asm.v1"xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<description> my appexe </description>
<trustInfoxmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel
level="asInvoker"
uiAccess="false"
/>
</requestedPrivileges>
</security>
</trustInfo>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows 8.1 -->
<supportedOSId="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
<!-- Windows Vista -->
<supportedOSId="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/>
<!-- Windows 7 -->
<supportedOSId="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
<!-- Windows 8 -->
<supportedOSId="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
</application>
</compatibility>
</assembly>
主要就是compatibility部分了,假设你已经有manifest文件了,仅仅须要加入compatibility部分就可以。对了Windows10怎么办?貌似[1]里面还没有说啊,别急,用
<!-- Windows 10 -->
<supportedOSId="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
就好了,怎么知道的?请叫我雷锋!在Windows10以下測试一把的结果如图。
兼容模式的影响
另一个可能的情况会造成GetVersionExW返回的系统版本号和实际的系统版本号不一样。这个与Windows8.1,Windows10没有什么关系。纯粹是为了兼容考虑,在设置兼容模式之后,GetVersionExW返回的是兼容的目标版本号的系统版本号。启动调试去查看应用程序的PEB是不是被改动过了,结果发现,并没有改动过PEB。那么问题来了,为什么GetVersionExW的值发生变化了呢?
直接调试GetVersionExW发现,在设置兼容模式之后,微软使用IATHook的方式,Hook了一堆的(嗯,不是1-2个,而是一堆)系统API,当中GetVersionExW就被AcLayers.dll里面的一个函数给Hook了,然后Hook函数里面返回了兼容系统版本号号。
如何推断兼容模式
一般来说,应用程序不须要推断当前是否处于兼容模式下执行,微软实现这个机制的目的本意就是想相应用程序透明。主要是非常多“古老的”程序内部严格限定仅仅能在某个详细的系统下执行,譬如限定在WindowsXP SP3下执行(由于当时微软的系统最高版本号可能就是XP),这样当用户操作系统升级之后,譬如升级到了Windows7,这个时候问题来了!本来普通情况下微软的系统是能够前向兼容的,结果应用程序自动不兼容,发现不是XP,主动退出,导致用户用不了了,因此微软发明了一个兼容模式,高版本号的系统能够模拟一个低版本号的系统执行环境,这样就解决大量的相似问题。
在兼容模式下,当应用程序调用GetVersionExW等API时,返回的是兼容的目标系统的系统版本号,当然这仅仅是兼容模式技术解决的一个问题而已,可是是较重要的一个问题(兼容模式还攻克了非常多其他问题)。
一般的应用程序不须要关心这个兼容模式。可是某些特殊的应用程序却恰恰须要,应用程序可能会依据不同的系统版本号做不同的事情,而一个可能性是用户误把应用程序设置为某个低版本号操作系统兼容执行,导致整个程序执行反而异常。
举个样例,像系统补丁修复程序,一般来说漏洞补丁都是和系统版本号一一相应,假设程序使用GetVersionExW来获取系统版本号,那么程序执行在Windows7以下,由于兼容模式的影响,导致补丁修复程序推送了一大批WindowsXP以下的补丁,想想这个场景,也是有点尴尬的。
从大部分的使用场景上面来说,放弃使用GetVersionExW或许是一个更好的选择。通过其他方式拿到更精确的系统版本号,不用考虑兼容模式的副作用,也不用操心Windows8(主要是指Window8.1和Window10)以上的系统获取到错误的系统版本号。
那么怎么推断当成程序正在兼容模式执行呢?方法应该有非常多,比較简单的方法,[4]里面介绍过一种,只是这样的方法要注意,在Windows8.1之后,它可能给出错误的结果,要依照上面提到的办法,让GetVersionExW返回正确的值。
第二种更好的方法是推断注冊表里面的应用程序兼容模式记录列表,当把一个应用程序设置为兼容模式或者管理员权限启动之后,系统会在HKEY_CURRENT_USER\Software\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\Layers以下记录相应的信息,假设想全部用户起效,则改动HKEY_LOCAL_MACHINE\\Software\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\Layers就可以。我们能够试试设置之后的效果。我在Windows7SP1以下任意设置了几个。
能够非常清楚的看到两个程序被设置为兼容WIN7RTM执行和兼容WINXPSP3执行,假设你去掉这两个注冊表值,则应用程序就不再以兼容模式执行,因此实际上能够检測这个位置推断哪些程序被设置为兼容模式执行,甚至能够通过删除这里的内容,去掉某些应用程序的兼容模式设置。同一时候能够发现的是微软用非常easy识别的字符串来描写叙述兼容的目标系统,很多其他的兼容描写叙述字符串能够參考[3],另外要注意的是从Windows8開始,这些字符串前面多了一个波浪线和空格(~ ),譬如兼容WINXPSP3,在Windows10以下是~ WINXPSP3。
推断系统版本号更好的办法
GetVersionExW既然被微软废弃了,再使用总认为拔凉拔凉的,有什么更好的推断系统版本号的方法吗?答案是肯定的!以下给出几种实践中用过的方法。
1、首先从原理上来说,GetVersionExW是读取的PEB里面的版本号信息,事实上我们自己也能够读取PEB嘛,仅仅是麻烦一点。这个就不给样例了。有兴趣能够自己实现一下。
2、微软在[1]里面事实上推荐过一批更好的API([7]),号称接口名更人性化,从名字上面看确实含义更清楚了,只是使用起来是否方便就仁者见仁智者见智了,任意罗列几个,只是这套API声明在<VersionHelpers.h>里面,比較新的SDK才有。
VERSIONHELPERAPI IsWindows7OrGreater()
VERSIONHELPERAPIIsWindows7SP1OrGreater()
VERSIONHELPERAPI IsWindows8OrGreater()
VERSIONHELPERAPI IsWindows8_1OrGreater()
VERSIONHELPERAPI IsWindowsServer()
3、使用VerifyVersionInfo来进行版本号推断(參考[8]),这个API声明在winbase.h里面,从Windows2000系统就已经開始提供了,可是我们可能非常少使用,说实话,使用起来不是特别方便。我们先看看是怎么使用的,它本质是进行版本号比較。
BOOL WINAPI VerifyVersionInfo(
_In_ LPOSVERSIONINFOEX lpVersionInfo,
_In_ DWORD dwTypeMask,
_In_ DWORDLONG dwlConditionMask
);
这个函数的原型里面第一个參数是熟悉的OSVERSIONINFOEX,可是这里是做为传入參数使用,第二个參数dwTypeMask用于指定要比較哪些项,能够比較主版本号,次版本号,Build号等等,能够使用位组合。第三个參数则是比較的方法,是>、=还是<,或者>=,<=等等,能够通过VER_SET_CONDITION来设置,能够进行各种组合来推断,还是比較灵活的。看两个样例吧。
BOOL IsWinVerGreaterThan(DWORDdwMajorVersion, DWORD dwMinorVersion)
{
OSVERSIONINFOEXW osvi = {0};
DWORDLONG dwlConditionMask = 0;
ZeroMemory(&osvi, sizeof(osvi));
osvi.dwOSVersionInfoSize= sizeof(osvi);
osvi.dwMajorVersion= dwMajorVersion;
osvi.dwMinorVersion= dwMinorVersion;
// 主版本号号推断
VER_SET_CONDITION(dwlConditionMask, VER_MAJORVERSION, VER_GREATER);
if (::VerifyVersionInfoW(&osvi, VER_MAJORVERSION, dwlConditionMask))
return TRUE;
// 次版本号号推断
VER_SET_CONDITION(dwlConditionMask, VER_MAJORVERSION, VER_EQUAL);
VER_SET_CONDITION(dwlConditionMask, VER_MINORVERSION, VER_GREATER);
return ::VerifyVersionInfo(&osvi, VER_MAJORVERSION | VER_MINORVERSION, dwlConditionMask);
}
//-------------------------------------------------------------------------
// 函数 : IsWinVerEqualTo
// 功能 : 推断是否=某个特定的系统版本号
// 返回值 : BOOL
// 參数 : DWORD dwMajorVersion
// 參数 : DWORD dwMinorVersion
// 附注 :
//-------------------------------------------------------------------------
BOOL IsWinVerEqualTo(DWORDdwMajorVersion, DWORD dwMinorVersion)
{
OSVERSIONINFOEXW osvi = {0};
DWORDLONG dwlConditionMask = 0;
// 1、初始化系统版本号信息数据结构
ZeroMemory(&osvi, sizeof(osvi));
osvi.dwOSVersionInfoSize= sizeof(osvi);
osvi.dwMajorVersion= dwMajorVersion;
osvi.dwMinorVersion= dwMinorVersion;
// 2、初始化条件掩码
VER_SET_CONDITION(dwlConditionMask, VER_MAJORVERSION, VER_EQUAL);
VER_SET_CONDITION(dwlConditionMask, VER_MINORVERSION, VER_EQUAL);
return ::VerifyVersionInfoW(&osvi, VER_MAJORVERSION | VER_MINORVERSION, dwlConditionMask);
}
封装一下使用就更方便了,譬如要推断当前是Window7,用IsWinVerEqualTo(6,1)就可以。或者你不想暴露一些“恶心”的MagicNumber,能够再封装一个IsWindows7()嘛。
4、另一个我个人比較喜欢的方法是使用一个未文档化的函数来获取系统版本号,也就是RtlGetNtVersionNumbers,这个是NTDLL里面的一个未文档化函数。可是这个函数微软把它导出了,因此我们就有办法使用了。
用法:
//-------------------------------------------------------------------------
// 函数 : GetNtVersionNumbers
// 功能 : 调用RtlGetNtVersionNumbers获取系统版本号信息
// 返回值 : BOOL
// 參数 : DWORD& dwMajorVer 主版本号
// 參数 : DWORD& dwMinorVer 次版本号
// 參数 : DWORD& dwBuildNumber build号
// 附注 :
//-------------------------------------------------------------------------
BOOL GetNtVersionNumbers(DWORD&dwMajorVer, DWORD& dwMinorVer,DWORD& dwBuildNumber)
{
BOOL bRet= FALSE;
HMODULE hModNtdll= NULL;
if (hModNtdll= ::LoadLibraryW(L"ntdll.dll"))
{
typedef void (WINAPI *pfRTLGETNTVERSIONNUMBERS)(DWORD*,DWORD*, DWORD*);
pfRTLGETNTVERSIONNUMBERS pfRtlGetNtVersionNumbers;
pfRtlGetNtVersionNumbers = (pfRTLGETNTVERSIONNUMBERS)::GetProcAddress(hModNtdll, "RtlGetNtVersionNumbers");
if (pfRtlGetNtVersionNumbers)
{
pfRtlGetNtVersionNumbers(&dwMajorVer, &dwMinorVer,&dwBuildNumber);
dwBuildNumber&= 0x0ffff;
bRet = TRUE;
}
::FreeLibrary(hModNtdll);
hModNtdll = NULL;
}
return bRet;
}
使用未文档化的函数要注意的一个点是,须要分析清楚函数的传入參数的类型,否则传错了类型,假设类型大小不一样,轻则函数出错,重则程序崩溃(尤其是传出參数)。我们能够看下RtlGetNtVersionNumbers这个函数是怎么实现的(调试用的ntdll.dll的版本号是6.1.7601.18247,其他系统的也差点儿相同的,仅仅是Hardcode的数字不一样),以下是它的实现伪码(IDA生成)。
int __stdcall RtlGetNtVersionNumbers(int a1, int a2, int a3)
{
int result; // eax@5
if ( a1 )
*(_DWORD *)a1 = 6;
if ( a2 )
*(_DWORD *)a2 = 1;
result = a3;
if ( a3 )
*(_DWORD *)a3 = 0xF0001DB1u;
return result;
}
我仅仅能说微软,你干得美丽!直接Hardcode处理,简单干净!
5、另一种方法是直接去获取NTDLL这个系统重要文件(其他的文件也可行,可是实践证明NTDLL最好)的文件版本号号,一般来说,该文件的版本号基本上就是系统的版本号。像[4]里面用到的推断兼容的方法就是通过对照GetVersionEx的返回值和关键系统文件的版本号,来推断是否当前应用程序处理兼容模式下
注:建议不要使用RtlGetVersion来进行版本号推断。Windows2003之前它的行为在兼容模式下和GetVersionExW不一致,Vista之后在兼容模式下它的行为和GetVersionExW一致。
效果展示
分别在WindowsXP,Windows7,Windows10以下測试了这些方法。注意左边的是常规模式执行,右边的是兼容模式执行。
參考文献
[1] Operating system version changes inWindows 8.1 and Windows Server 2012 R2 http://msdn.microsoft.com/en-us/library/windows/desktop/dn302074(v=vs.85).aspx
[2] GetVersionExhttp://msdn.microsoft.com/en-us/library/ms724451(VS.85).aspx
[3] Running an Application asAdministrator or in Compatibility Mode http://www.verboon.info/2011/03/running-an-application-as-administrator-or-in-compatibility-mode/
[4] http://blog.csdn.net/magictong/article/details/5829065
[5] http://blogs.msdn.com/b/chuckw/archive/2013/09/10/manifest-madness.aspxMadness
[6] OSVERSIONINFOEX structure http://msdn.microsoft.com/en-us/library/ms724833(v=vs.85).aspx
[7] Version Helper functions http://msdn.microsoft.com/en-us/library/windows/desktop/dn424972(v=vs.85).aspx
[8] VerifyVersionInfofunction http://msdn.microsoft.com/en-us/ms725492(VS.85).aspx