如果主机上安装的是四核CPU,那么Windows任务管理器应当显示出4个CPU的性能统计图表。本章将要稍微hack 任务管理器,让它显示更多的CPU运算核心。
首先需要了解的问题是:任务管理器如何知道CPU有多少个运算核心?虽然运行于win32用户空间的GetSystemInfo()函数确实可以反馈这一信息,但是任务管理器taskmgr.exe没有直接导入这个函数。它多次调用NTAPI中的NtQuerySystemInformation()函数获取的各种系统信息,也是通过后者了解CPU的具体情况。
NtQuerySystemInformation()函数有四个参数:第一个参数是查询的系统信息类型[1];第二个参数是一个指针,这个指针用来返回系统的HandleList;第三个参数是程序员指定分配给HandleList的内存空间大小;第四个参数是NtQuerySystemInformation返回的HandleList的大小。
要获取CPU信息,就要在调用它的时候把第一个参数设置为常量SystemBasicInformation[2]。
因此,我们要查找的调用指令大体会是“NtQuerySystemInformation(0, ?, ?, ?)”。第一步当然是用IDA打开taskmgr.exe文件。在处理微软的官方程序时,IDA能够下载与之相应的PDB文件并显示全部函数名称。显而易见的是,任务管理器是用C++编写的程序,而且它使用的函数名称和类(class)名称真的是不为人知。在它使用的类名称里,我们可以看到CAdapter、CNetPage、CPerfPage、CProcInfo、CProcPage、CSvcPage、CTaskPage和CUserPage。这些类名称和任务管理器程序窗口的标签(tab)有对应关系。
在跟踪了NtQuerySystemInformation()函数的每次调用过程之后,我们可以统计出传递给函数的第一个参数。在图74.1中可以看到,部分调用过程里的第一个参数值明显不是零,所以被标记上了“Not Zero”。另外,还有一些函数调用的情况非常特殊,本章的第二部分再进行有关讲解。总之,我们要找那些“第一个参数是零”的、NtQuerySystemInformation()函数的调用语句。
图74.1 IDA:NtQuerySystemInformation()函数的xrefs
那些不公开的名称暂且放置一边。
在检索“NtQuerySystemInformation(0, ?, ?, ?)”的调用语句时,我们可以很快地在InitPerfInfo()里找到如下所示的这种语句。
指令清单74.1 taskmgr.exe (Windows Vista)
.text:10000B4B3 xor r9d, r9d
.text:10000B4B6 lea rdx, [rsp+0C78h+var_C58] ; buffer
.text:10000B4BB xor ecx, ecx
.text:10000B4BD lea ebp, [r9+40h]
.text:10000B4C1 mov r8d, ebp
.text:10000B4C4 call cs:__imp_NtQuerySystemInformation ; 0
.text:10000B4CA xor ebx, ebx
.text:10000B4CC cmp eax, ebx
.text:10000B4CE jge short loc_10000B4D7
.text:10000B4D0
.text:10000B4D0 loc_10000B4D0: ; CODE XREF: InitPerfInfo(void)+97
.text:10000B4D0 ; InitPerInfo(void)+AF
.text:10000B4D0 xor al, al
.text:10000B4D2 jmp loc_10000B5EA
.text:10000B4D7 ; ----------------------------------------------------------------------------
.text:10000B4D7
.text:10000B4D7 loc_10000B4D7: ; CODE XREF: InitPerfInfo(void)+36
.text:10000B4D7 mov eax, [rsp+0C78h+var_C50]
.text:10000B4DB mov esi, ebx
.text:10000B4DD mov r12d, 3E80h
.text:10000B4E3 mov cs:?g_PageSize@@3KA, eax ; ulong g_PageSize
.text:10000B4E9 shr eax, 0Ah
.text:10000B4EC lea r13, __ImageBase
.text:10000B4F3 imul eax, [rsp+0C78h+var_C4C]
.text:10000B4F8 cmp [rsp+0C78h+var_C20], bpl
.text:10000B4FD mov cs:?g_MEMMax@@3_JA, rax ; __int64 g_MEMMax
.text:10000B504 movzx eax, [rsp+0C78h+var_C20] ; no. of CPUs
.text:10000B509 cmova eax, ebp
.text:10000B50C cmp al, bl
.text:10000B50E mov cs:?g_cProcessors@@3EA, al ; uchar g_cProcessors
从微软的服务器上下载相应的PDB文件之后,IDA就能够给各个变量分配正确的变量名称。我们不难从中找到全局变量g_cProcessors。
传递给NtQuerySystemInformation()函数的第二个参数(即接收缓冲区)是var_C58。var_C20和var_C58之间的地址差值是0xC58−0xC20=0x38(56)。根据MSDN的官方说明,可知返回值的数据格式如下:
typedef struct _SYSTEM_BASIC_INFORMATION {
BYTE Reserved1[24];
PVOID Reserved2[4];
CCHAR NumberOfProcessors;
} SYSTEM_BASIC_INFORMATION;
因为本例是在x64系统上的演示,所以PVOID占用8个字节。两个“reserved”保留字段共占用24+4×8=56字节。这意味着var_C20很可能就是_SYSTEM_BASIC_INFORMATION里的NumberOfProcessors字段。
下面我们来验证这一推论。把C:\Windows\System32里的taskmgr.exe复制出来,然后我们在对复制品进行修改,以防Windows的文件保护机制自动恢复原始文件。
使用Hiew打开复制出来的文件,然后找到图74.2所示的程序地址。
图74.2 Hiew:找到修改点
接下来替换MOVZX指令,通过MOV指令直接把返回结果改为64(将CPU设为64核)。由于修改后的指令比原始指令短1个字节,所有我们还需添加1个NOP指令。如图74.3所示。
图74.3 Hiew:修改程序
修改后的这个程序可以正常运行!当然,图表中的统计信息肯定是不正确的。CPU的总负载偶尔还会超过100%。如图74.4所示。
图74.4 被骗的Windows任务管理器
刚才我们把CPU运算核心的总数改为了64(更大的值会使任务管理器崩溃)。显然Windows Vista的任务管理器无法在拥有更多运算核心的计算机上运行。这也可能是微软通过静态的数据结构把有关数值限定在64以下的原因。
任务管理器taskgmr.exe传递NtQuerySystemInformation()第一个参数的指令并非都是MOV指令,部分指令是LEA。
指令清单74.2 taskmgr.exe (Windows Vista)
xor r9d, r9d
div dword ptr [rsp+4C8h+WndClass.lpfnWndProc]
lea rdx, [rsp+4C8h+VersionInformation]
lea ecx, [r9+2] ; put 2 to ECX
mov r8d, 138h
mov ebx, eax
; ECX=SystemPerformanceInformation
call cs:__imp_NtQuerySystemInformation ; 2
...
mov r8d, 30h
lea r9, [rsp+298h+var_268]
lea rdx, [rsp+298h+var_258]
lea ecx, [r8-2Dh] ; put 3 to ECX
; ECX=SystemTimeOfDayInformation
call cs:__imp_NtQuerySystemInformation ; not zero
...
mov rbp, [rsi+8]
mov r8d, 20h
lea r9, [rsp+98h+arg_0]
lea rdx, [rsp+98h+var_78]
lea ecx, [r8+2Fh] ; put 0x4F to ECX
mov [rsp+98h+var_60], ebx
mov [rsp+98h+var_68], rbp
; ECX=SystemSuperfetchInformation
call cs:__imp_NtQuerySystemInformation ; not zero
出现这种指令的具体原因不明。但是MSVC编译器还是经常如此分配指令。或许LEA指令会带来速度或性能方面的好处吧。
您还可以在指令清单64.7(64.5.1节)里看到这种情况。
[1] 后面提到的HandleList就是函数应当反馈的类型信息。
[2] 这个常量的值为零。更多信息请参考MSDN: https://msdn.microsoft.com/en-us/library/ windows/desktop/ms724509(v=vs.85).aspx。