(转载)Win32 常见问题解答

来源:互联网 发布:linux 远程访问数据库 编辑:程序博客网 时间:2024/05/29 06:51

Jeffrey Richter

:我正在开发一个基于Windows NT®的服务程序。我的程序同任何程序一样存在一些小的毛病。当我的服务程序出错以后,我想杀掉这个服务程序将它重新启动。我经常使用任务管理器来杀掉我不再想使用的进程。但是,如果我想杀掉一个服务程序的话,显示在我面前的就是一个权限被拒绝的消息框,有什么办法能够让任务管理器来杀掉一个服务程序吗?
Andrea DeMaio
Pacific Grove, CA

A. 在我回答这个问题以前,让我们先来研究以下为什么任务管理器会显示一个消息框(如图1所示)。当你在任务管理器中选择了一个进程并且点击了终止进程的按纽,任务管理器将会获取所选进程的ID并执行下面的伪代码:

DWORD dwProcessId = (Initialized from the ListView                      control);HANDLE hProcess = OpenProcess(PROCESS_TERMINATE, FALSE,                              dwProcessId);if (hProcess == NULL) {   // Display error message box (like in Figure 1).} else {   TerminateProcess(hProcess, 1);   CloseHandle(hProcess);}


因为OpenProcess这个函数不能够打开特定进程的句柄,所以消息框弹出。这个操作失败是因为服务程序是运行在本机系统安全帐户下,而你哪怕是以管理员的身份登录,也没有足够的权限来打开这个服务程序的句柄。事实上,在OpenProcess返回空并收到一个ERROR_ACCESS_DENIED (5)这样的错误代码后,任务管理器会立即调用GetLastError。任务管理器将代表这个错误的文本放在消息框中。



图 1: 错误消息框

微软允许进程通过使用特权的方式越过这个权限检查。在Windows NT中,特权是分配给用户的属性,这个属性允许用户超越对操作系统某个部分的所谓限制。Windows NT 4.0 支持24种特权允许做一些类似备份恢复文件等通常不会被授予权限的操作,还可以改变系统时间,或是在实时优先级上运行一个进程,以及关闭系统的操作。

其中有一种特权叫做SeDebugPrivilege。通常,操作系统不允许用户对进程进行跟踪,是因为系统不想使用户(或黑客)具有改变一个运行中的进程的能力。然而,有一些用户需要这种对进程进行跟踪调试的能力。缺省情况下,Windows NT自动授予管理员跟踪调试的特权。你可以通过运行User Manager管理工具进行验证。选择Policies 下面的User Rights菜单选项,点击 Show Advanced User Rights checkbox,然后选择Debug programs 的权力 (如图2所示)。



图 2: 用户权限策略

如果你在登录后被授予了跟踪程序的特权,你就可以对应用程序进行跟踪调试。什么意思呢?就是说,想要对一个应用程序进行跟踪调试,你必须能够被允许成功调用OpenProcess这个函数,这样你就能够调用各种与调试器相关的API,如ReadProcessMemory、 WriteProcessMemory、 VirtualQueryEx、 VirtualProtectEx、 VirtualAllocEx、VirtualFreeEx 、CreateRemoteThread,等等。所有这些功能都需要一个进程的句柄才能正确的运行。简单地说, 尽管你通常情况下不能够打开一个进程,如果你具有了调试特权,你就能够强迫OpenProcess 打开一个进程。

读了以上这些,你可能仍然很迷惑。你可能在对自己说: "怪了,我就是以管理员的身份登录的,可是当我告诉任务管理器我要终止一个服务进程时,我看到的仍然是一个权限被拒绝的消息框,这是为什么?"拥有了特权就意味着可以超越普通的操作,具有对强大的系统功能的无限制的权限。然而,系统要保护普通的用户,不允许管理员蓄意或无意地使系统崩溃,影响到其他用户。我们可以很容易地想象一个场景,假设一个管理员登录进入系统,然后告诉任务管理器要杀掉WinLogon.exe 或 CSRSS.exe 进程。这些进程是系统正常运转的必要条件,它们必须始终运行在系统中,否则将会造成整个系统的崩溃。正因如此,任务管理器不会允许一个管理员在无意之中终止这些进程,使整个系统瘫痪。

用户与权限之间有三种关系:被拒绝、被授权和能使用。"被拒绝"以为着用户永远不具有对系统的某个组件超越其所限制权限的能力。例如,如你所看到的所有管理员以外的用户都不具有跟踪调试的特权,所以一个普通的用户就永远不能够调试一个进程。对于系统的许多用户来说,绝大多数特权都是被拒绝的。管理员可以使用User Manager工具给用户授予特权。不过要注意:用户必须退出系统(如果当前是登录状态的话),以新分配的特权重新登录,授权才能生效。

"被授权"意味着用户可以利用特权运行程序。但是请记住特权给了用户一个执行通常被限制的强大操作的权限。这本身是一件非常危险的事情,所以不应当轻易获取。所以,就算一个用户可能被授予特权,系统可能还会在很明白地赋予它使用权以前保留这个特权,在这时,用户可能还是行使不了这个权限。

"能使用"意味着用户已经被赋予特权而且特权已经开放。那么现在,当你要执行一个受限制的操作时,系统会进行检查,看看这个用户是被赋予了哪种特权的使用权(不仅仅是授权),确认后,用户就可以超越这个限制。这就是为什么任务管理器不允许管理员终止一个服务程序的原因,尽管管理员已经被授予了调试特权,任务管理器没有将它开放。

当你登录进系统以后, Windows NT创建了一个叫做访问标志的对象。这个对象包括许多信息,包括那些特权被授权或可使用。访问标志与shell进程Explorer.exe相关联。一旦你运行了一个新的进程,系统会将父进程的访问标志复制赋予新的进程。换句话说,就是每个进程都有它自己的特权集。也就是说,如果你为一个进程赋予了一个特权的使用权,这个使用权不会对其他进程开放除非你明确地赋予。所以,如果你对任务管理进程开放了跟踪调试特权的使用权,任务管理器将能够终止服务进程。

图三显示了一个小程序, EnableDebugPrivAndRun,这个程序演示了如何运行一个具有跟踪调试特权使用权的进程。运行任务管理器(它的调试特权开放),编译链接这个程序使用这样的命令行来运行它:
C:/>EnableDebugPrivAndRun.exe TaskMgr.exe

当你执行这个命令行以后, EnableDebugPrivAndRun 首先调用 OpenProcessToken。它返回一个与EnableDebugPrivAndRun 进程相连的访问标志的句柄。如果成功得到访问标志的句柄,调用EnablePrivilege-我自己的函数,功能只是把所有必要的赋予或不赋予特权的步骤打包。

图3: EnableDebugPrivAndRun.cpp/*************************************************************Module name: EnableDebugPrivAndRun.cppNotices: Written 1998 by Jeffrey RichterDescription: Enables the Debug privilege before running an app**** *********************************************************/#define STRICT#include //////////////////////////////////////////////////////////////BOOL EnablePrivilege(HANDLE hToken, LPCTSTR szPrivName,                      BOOL fEnable) {   TOKEN_PRIVILEGES tp;   tp.PrivilegeCount = 1;   LookupPrivilegeValue(NULL, szPrivName, &tp.Privileges[0].Luid);   tp.Privileges[0].Attributes = fEnable ? SE_PRIVILEGE_ENABLED : 0;   AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), NULL, NULL);   return((GetLastError() == ERROR_SUCCESS));}//////////////////////////////////////////////////////////////int WINAPI WinMain(HINSTANCE hinstExe, HINSTANCE hinstExePrev,                    LPSTR pszCmdLine, int nCmdShow) {   HANDLE hToken;   if (OpenProcessToken(GetCurrentProcess(),                         TOKEN_ADJUST_PRIVILEGES, &hToken)) {      if (EnablePrivilege(hToken, SE_DEBUG_NAME, TRUE)) {         if (ShellExecute(NULL, NULL, pszCmdLine, NULL,             NULL, SW_SHOWNORMAL) < (HINSTANCE) 32) {            MessageBox(NULL, pszCmdLine,                        __TEXT("EnableDebugPrivAndRun: Couldn't run"),                       MB_OK | MB_ICONINFORMATION);         }      }      CloseHandle(hToken);   }   return(0);}//////////////////////// End Of File /////////////////////////

EnablePrivilege 需要三个参数:包括用户特权信息的访问标志的句柄、标识你想改变的特权的字符串和一个布尔型变量标识你是否要赋予这个特权。带着这三个参数, EnablePrivilege 对TOKEN_PRIVILEGES 结构进行初始化。这个结构实际上是一个可变长度的结构,它允许你同时说明几个特权。由于EnablePrivilege 函数只允许你一次改变一个特权,所以代码就比同时能改变多个特权的代码简单。

LookupPrivilegeValue函数将特权名字转换成一个64位的等价数值叫做LUID(本地独有标识)。对许多开发者来说这很奇怪。通常,开发人员喜欢使用数字而不是字符串;但是使用特权时,通常是以字符串来方式进行的,然后再调用LookupPrivilegeValue 将这个字符串转化为数字。LUID,保证是再本地系统中独一无二的标识。你不能够在机器之间传递一个LUID,甚至,你不能在同一台机器上经过重新启动后还保留一个LUID。换句话说,如果你在两次调用之间重新启动系统,将调试特权字符串传递给LookupPrivilegeValue将会导致返回两个不同的LUID。

一旦你拥有了调试特权的LUID, EnablePrivilege将调用AdjustTokenPrivileges赋予或取消特权。你应当注意到当我调用OpenProcessToken时,我要申请TOKEN_ADJUST_PRIVILEGES权限, 这时成功调用AdjustTokenPrivileges的权限。

在EnablePrivilege 返回以后,我将EnableDebugPrivAndRun的命令行传递给ShellExecute 函数。ShellExecute 将会在内部调用CreateProcess 生成一个新的进程。记住权限标识是有继承性的,所以这个新的进程将会继承EnableDebugPrivAndRun的权限标识,现在这个标识表示调试特权使用权开放。这意味着新进程的调试使用权也是开放的能够成功地调用OpenProcess。

我喜欢运行在我系统上的任务管理器所以我经常通过查看任务栏上始终左边的小图标来查看CPU的性能。这以下是我为了确信任务管理器总是在调试特权开放的情况下运行所做的工作。首先,我建立了我的EnableDebugPrivAndRun程序并将它放在一个目录下面。然后我创建一个快捷方式让任务管理器一开始以窗口最小化的方式运行。我把这个快捷方式的.LNK 文件放在与EnableDebugPrivAndRun 程序所在的同一目录下。然后再创建另一个快捷方式运行EnableDebugPrivAndRun,把任务管理器快捷方式的文件名作为参数传递给它。这个快捷方式的命令行字符串如下所示:

C:/Bin/EnableDebugPrivAndRun.exe "C:/Bin/Task Manager.lnk"

我把第二个快捷方式放在我的开始菜单的Startup文件夹中这样系统就可以在我一登录进去就将它激活。就是这么简单!

:在你的那本Advanced Windows(127页)中,你提到当Windows 95 或Windows NT从软盘运行一个可执行的映像文件时,系统实际上是将整个文件的映像装入RAM中。系统这样做是为了能让盘片从驱动器里取出是不影响应用程序。
这看起来是个好主意,我很高兴微软把这个功能放进操作系统中。我还能想象出许多种这样做的好处。例如,当映像文件位于网络驱动器或是光盘驱动器时,这样做也很有必要。我还想让文件在压缩或加密时装入到RAM中(Windows NT5.0)。有没有一种方法能让我强制系统将某一指定的文件映像装入到RAM中?
Frank Merrow

: Windows NT 4.0启动时,微软在装载程序中加入这样的支持。当你创建了一个可执行的映像时,(.EXE或DLL),你可以在链接器中加入以下的开关说明。:

/SWAPRUN:NET

这个开关告诉链接器打开某个标志位。当装载程序装载你的映像文件时,装载文件要检查这个位是否已经设为开,还有映像是否是从网络驱动器装载。如果是,则装载程序将文件的映像装入RAM以防止在网络上频繁交换。

链接器还支持这种格式的SWAPRUN 开关:

/SWAPRUN:CD

这个开关告诉装载程序如果文件映像是在光盘驱动器中运行的,则强制将映像装入RAM中。注意要想让这些开关工作,你的链接器必须支持这个开关而装入程序必须认识这个开关作出正确的响应。Windows NT 4.0及以后的版本都能认识/SWAPRUN:NET 和 /SWAPRUN:CD 位;Windows? 95 不能。

现在,装载程序不支持将压缩或加密的映像文件装入到RAM中。但是自己编制一个强行将整个映像文件装入RAM的函数是非常简单的。图4显示了一个这样的函数-RunImageLocally。这个函数只有一个参数,就是可执行映像的HINSTANCE (或 HMODULE) 。这个参数也要传递给你的WinMain 函数, DllMain 函数,从LoadLibrary这个调用函数中返回的也是这个值。函数然后循环调用VirtualQuery检查包括可执行映像的所有内存区域。

图 4: RunImageLocally.cpp/******************************************************************************Module name: RunImageLocally.cppNotices: Written 1998 by Jeffrey RichterDescription: Forces an executable image to be run from the paging file.******************************************************************************/#include ///////////////////////////////////////////////////////////////////////////////void WINAPI RunImageLocally(HINSTANCE hinst) {   DWORD cp= 0;   static DWORD s_dwPageSize = 0;   // Get the system's page size (do this only once)   if (s_dwPageSize == 0) {      SYSTEM_INFO si;      GetSystemInfo(&si);      s_dwPageSize = si.dwPageSize;   }   PBYTE pbAddr = (PBYTE) hinst;   MEMORY_BASIC_INFORMATION mbi;   VirtualQuery(pbAddr, &mbi, sizeof(mbi));   // Perform this loop until we find a region beyond the end of the file.   while (mbi.AllocationBase == hinst) {      // We can only force committed pages into RAM.      // We do not want to trigger guard pages and confuse the application.      if ((mbi.State == MEM_COMMIT) && ((mbi.Protect & PAGE_GUARD) == 0)) {         // Determine if the pages in this region are nonwriteable         BOOL fNonWritableRgn =             (mbi.Protect == PAGE_NOACCESS) ||             (mbi.Protect == PAGE_READONLY) ||             (mbi.Protect == PAGE_EXECUTE)  ||             (mbi.Protect == PAGE_EXECUTE_READ);         DWORD dwOrigProtect = 0;         if (fNonWritableRgn) {            // Nonwriteable region, make it writeable (with the least protection)            VirtualProtect(mbi.BaseAddress, mbi.RegionSize,                            PAGE_EXECUTE_READWRITE, &dwOrigProtect);         } else {            // This is a writeable page, we can leave the protections alone.         }         // Write to every page in the region.         // This forces the page to be in RAM and swapped to the paging file.         for (DWORD cbRgn = 0; cbRgn < mbi.RegionSize; cbRgn += s_dwPageSize) {            // Volatile so that the optimizer won't remove this code            volatile PDWORD pdw = (PDWORD) &pbAddr[cbRgn];              *pdw = *pdw;            cp++;         }         if (dwOrigProtect != 0) {            // If we changed the protection, change it back            VirtualProtect(mbi.BaseAddress, mbi.RegionSize, dwOrigProtect,                            &dwOrigProtect);         }      }      // Get next region      VirtualQuery(pbAddr += mbi.RegionSize, &mbi, sizeof(mbi));   }   GetLastError();}///////////////////////////////////////////////////////////////////////////////int WINAPI WinMain(HINSTANCE hinstExe, HINSTANCE hinstExePrev,                    LPSTR lpCmdLine, int nCmdShow) {   RunImageLocally(hinstExe);   return(0);}///////////////////////////////// End Of File /////////////////////////////////

对于每个区域,检查页是否被提交及是否是保护页。如果页还没有被提交,则说明没有要写入RAM中的内容。如果页有PAGE_GUARD 属性,对它进行操作将会引起一个STATUS_GUARD_PAGE_VIOLATION 异常,如果你不想让你的应用程序混乱,就也不能够将这些页写到RAM中。

对于这些所有的页,我检查它们看是否是可写的。如果不是,我将改变对这些页的保护以使我能够对它们进行写操作。我使用 PAGE_EXECUTE_READWRITE 因为这是所有的保护中最自由的一种。一旦我知道页是可写的,我就能在一个区域的每一页中都写入单个的DWORD。执行这个写操作令系统把RAM中的页做一个拷贝。在区域中这些所有的页都被写了之后,我将页的保护方式改回到原来的方式(如果必要的话)。最后,重复这个操作直到包含可执行映像文件的所有区域都完成为止。

应当注意的是:这个程序不是线程安全的,也没有办法使它安全。进程中的其他线程有可能会改变会改变文件映像所在页的保护方式或者改动页中的数据。仅仅是执行代码或碰触数据根本就不是问题。但是如果其他线程改变了页的保护方式,这将是个潜在的问题。

我只在Windows NT上测试了这段代码,但是它应当在Windows 95一样运行良好。由于Windows 95不支持写时拷贝的机制,所以在Windows 95 上使用这项技术就不大合适。当你将一个映像装入到Windows 95上时,装载程序会为映像文件的可写页自动分配RAM (及交换文件区) 。

原创粉丝点击