核心编程随笔5
Note 0:
Windows提供了一个作业(job)内核对象,它允许你将进程组合在一起并创建一个"沙箱"来限制进程能够做什么.最好将作业对象想象成一个进程容器.但是,即使作业中只包含一个进程,也是非常有用的,因为这样可以对进程施加平时不能施加的限制.
Note 1:
以下的StartRestrictedProcess函数将一个进程放入一个作业中,以限制此进程具体能够做哪些事情,如下所示:
void StartRestrictedProcess() {
// Check if we are not already associated with a job.
// If this is the case, there is no way to switch to
// another job.
BOOL bInJob = FALSE;
IsProcessInJob(GetCurrentProcess(), NULL, &bInJob);
if (bInJob) {
MessageBox(NULL, TEXT("Process already in a job"),
TEXT(""), MB_ICONINFORMATION | MB_OK);
return;
}
// Create a job kernel object.
HANDLE hjob = CreateJobObject(NULL,
TEXT("Wintellect_RestrictedProcessJob"));
// Place some restrictions on processes in the job.
// First, set some basic restrictions.
JOBOBJECT_BASIC_LIMIT_INFORMATION jobli = { 0 };
// The process always runs in the idle priority class.
jobli.PriorityClass = IDLE_PRIORITY_CLASS;
// The job cannot use more than 1 second of CPU time.
jobli.PerJobUserTimeLimit.QuadPart = 10000; // 1 sec in 100-ns intervals
// These are the only 2 restrictions I want placed on the job (process).
jobli.LimitFlags = JOB_OBJECT_LIMIT_PRIORITY_CLASS
| JOB_OBJECT_LIMIT_JOB_TIME;
SetInformationJobObject(hjob, JobObjectBasicLimitInformation, &jobli,
sizeof(jobli));
// Second, set some UI restrictions.
JOBOBJECT_BASIC_UI_RESTRICTIONS jobuir;
jobuir.UIRestrictionsClass = JOB_OBJECT_UILIMIT_NONE; // A fancy zero
// The process can't log off the system.
jobuir.UIRestrictionsClass |= JOB_OBJECT_UILIMIT_EXITWINDOWS;
// The process can't access USER objects (such as other windows)
// in the system.
jobuir.UIRestrictionsClass |= JOB_OBJECT_UILIMIT_HANDLES;
SetInformationJobObject(hjob, JobObjectBasicUIRestrictions, &jobuir,
sizeof(jobuir));
// Spawn the process that is to be in the job.
// Note: You must first spawn the process and then place the process in
// the job. This means that the process’ thread must be initially
// suspended so that it can’t execute any code outside of the job's
// restrictions.
STARTUPINFO si = { sizeof(si) };
PROCESS_INFORMATION pi;
TCHAR szCmdLine[8];
_tcscpy_s(szCmdLine, _countof(szCmdLine), TEXT("CMD"));
BOOL bResult =
CreateProcess(
NULL, szCmdLine, NULL, NULL, FALSE,
CREATE_SUSPENDED | CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi);
// Place the process in the job.
// Note: If this process spawns any children, the children are
// automatically part of the same job.
AssignProcessToJobObject(hjob, pi.hProcess);
// Now we can allow the child process' thread to execute code.
ResumeThread(pi.hThread);
CloseHandle(pi.hThread);
// Wait for the process to terminate or
// for all the job's allotted CPU time to be used.
HANDLE h[2];
h[0] = pi.hProcess;
h[1] = hjob;
DWORD dw = WaitForMultipleObjects(2, h, FALSE, INFINITE);
switch (dw – WAIT_OBJECT_0) {
case 0:
// The process has terminated...
break;
case 1:
// All of the job's allotted CPU time was used...
break;
}
FILETIME CreationTime;
FILETIME ExitTime;
FILETIME KernelTime;
FILETIME UserTime;
TCHAR szInfo[MAX_PATH];
GetProcessTimes(pi.hProcess, &CreationTime, &ExitTime,
&KernelTime, &UserTime);
StringCchPrintf(szInfo, _countof(szInfo), TEXT("Kernel = %u | User = %u\n"),
KernelTime.dwLowDateTime / 10000, UserTime.dwLowDateTime / 10000);
MessageBox(GetActiveWindow(), szInfo, TEXT("Restricted Process times"),
MB_ICONINFORMATION | MB_OK);
// Clean up properly.
CloseHandle(pi.hProcess);
CloseHandle(hjob);
}
Note 2:
IsProcessInJob函数可以验证当前进程是否在一个现有的作业控制之下运行:
BOOL IsProcessInJob(
HANDLE hProcess,
HANDLE hJob,
PBOOL pbInJob);
Note 3:
默认情况下,在Windows Vista中通过Windows资源管理器来启动一个应用程序时,进程会自动同一个专用的作业关联,此作业的名称使用了"PCA"字符串前缀.作业中的一个进程退出时,我们是可以接收到一个通知的.所以,一旦通过Windows资源管理器启动的一个历史遗留的程序出现问题,就会触发Program Compatibility Assistant(程序兼容性助手).
Windows Vista提供这个功能的目的是检测兼容性问题.所以,如果你已经为应用程序定义了一个清单(manifest),Windows资源管理器就不会将你的进程同"PCA"前缀的作业关联,它会假定你已经解决了任何可能的兼容性问题.
但是,在需要调试应用程序的时候,如果调试器是从Windows资源管理器启动的,即使有一个清单(mainifest),应用程序也会从调试器继承带有"PCA"前缀的作业.一个简单的解决方案是从命令行而不是Windows资源管理器中启动调试器.在这种情况下,不会发生与作业的关联.
Note 4:
CreateJobObject函数可以用来创建一个新的作业内核对象:
HANDLE CreateJobObject(
PSECURITY_ATTRIBUTES psa,
PCTSTR pszName);
OpenJobObject函数可以用来打开一个作业内核对象:
HANDLE OpenJobObject(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
PCTSTR pszName);
Note 5:
关闭作业的句柄会导致所有进程都不可访问此作业,即使这个作业仍然存在.如以下代码所示:
// Create a named job object.
HANDLE hJob = CreateJobObject(NULL, TEXT("Jeff"));
// Put our own process in the job.
AssignProcessToJobObject(hJob, GetCurrentProcess());
// Closing the job does not kill our process or the job.
// But the name ("Jeff") is immediately disassociated with the job.
CloseHandle(hJob);
// Try to open the existing job.
hJob = OpenJobObject(JOB_OBJECT_ALL_ACCESS, FALSE, TEXT("Jeff"));
// OpenJobObject fails and returns NULL here because the name ("Jeff")
// was disassociated from the job when CloseHandle was called.
// There is no way to get a handle to this job now.
Note 6:
创建好一个作业之后,接着一般需要限制作业中的进程能做的事情;换言之,现在要设置一个"沙箱".可以向作业应用以下几种类型的限制:
基本限制和扩展基本限制,防止作业中的进程独占系统资源.
基本的UI限制,防止作业内的进程更改用户界面.
安全限制,防止作业内的进程访问安全资源(文件、注册表子项等).
Note 7:
SetInformationJobObject函数可以向作业应用限制:
BOOL SetInformationJobObject(
HANDLE hJob,
JOBOBJECTINFOCLASS JobObjectInformationClass,
PVOID pJobObjectInformation,
DWORD cbJobObjectInformationSize);
限制类型
限制 第二个参数的值 第三个参数的结构
类型
基本限制 JobObjectBasicLimitInformation JOBOBJECT_BASIC_LIMIT_INFORMATION
扩展后的基本限制 JobObjectExtendedLimitInformation JOBOBJECT_EXTENDED_LIMIT_INFORMATION
基本的UI限制 JobObjectBasicUIRestrictions JOBOBJECT_BASIC_UI_RESTRICTIONS
安全限制 JobObjectSecurityLimitInformation JOBOBJECT_SECURITY_LIMIT_INFORMATION
Note 8:
针对作业对象基本用户界面限制的位标志
标志 描述
JOB_OBJECT_UILIMIT_EXITWINDOWS 阻止进程通过ExitWindowsEx函数注销、关机、重启或断开系统电源.
JOB_OBJECT_UILIMIT_READCLIPBOARD 阻止进程读取剪贴板中的内容
JOB_OBJECT_UILIMIT_WRITECLIPBOARD 阻止进程清除剪贴板中的内容
JOB_OBJECT_UILIMIT_SYSTEMPARAMETERS 阻止进程通过SystemParametersInfo 函数更改系统参数
JOB_OBJECT_UILIMIT_DISPLAYSETTINGS 阻止进程通过ChangeDisplaySettings函数更改显示设置
JOB_OBJECT_UILIMIT_GLOBALATOMS 为作业指定其专有的全局atom表,并规定作业中的进程只能访问作业的表
JOB_OBJECT_UILIMIT_DESKTOP 阻止进程使用CreateDesktop或SwitchDesktop函数来创建或切换桌面
JOB_OBJECT_UILIMIT_HANDLES 阻止作业中的进程使用同一个作业外部的进程所创建的USER对象(比如HWND)
Note 9:
有时需要让作业内部的一个进程同作业外部的一个进程通信.一个简单的办法是使用窗口消息.但是,如果作业中的进程不能访问UI句柄,那么作业内部的进程就不能向作业外部的进程创建的一个窗口发送或张贴窗口消息.幸运的是,可以用另一个函数来解决这个问题,如下所示:
BOOL UserHandleGrantAccess(
HANDLE hUserObj,
HANDLE hJob,
BOOL bGrant);
Note 10:
QueryInformationJobObject函数能查询对作业施加的限制:
BOOL QueryInformationJobObject(
HANDLE hJob,
JOBOBJECTINFOCLASS JobObjectInformationClass,
PVOID pvJobObjectInformation,
DWORD cbJobObjectInformationSize,
PDWORD pdwReturnSize);
向此函数传递作业的句柄(这类似于SetInformationJobObject函数)——这是一个枚举类型,它指出了你希望哪些限制信息,要由函数初始化的数据结构的地址,以及包含该数据结构的数据块的大小.最后一个参数是pdwReturnSize,它指向由此函数来填充的一个DWORD,指出缓冲区中已填充了多少个字节.如果对这个信息不在意,可以(通常也会)为此参数传递一个NULL值.
作业中的进程可以调用QueryInformationJobObject获得其所属作业的相关信息(为作业句柄参数传递NULL值).这是很有用的一个技术,因为它使进程能看到自己被施加了哪些限制.不过,如果为作业句柄参数传递NULL值,SetInformationJobObject函数调用会失败——目的是防止进程删除施加于自己身上的限制.
Note 11:
AssignProcessToJobObject函数可以将进程显式地放入我新建的作业中:
BOOL AssignProcessToJobObject(
HANDLE hJob,
HANDLE hProcess);
这个函数只允许将尚未分配给任何作业的一个进程分配给一个作业,你可以使用IsProcessInJob函数对此进行检查.
Note 12:
一旦进程已经属于作业的一部分,它就不能再移动到另一个作业中,也不能成为所谓"无作业"的.还要注意,当作业中的一个进程生成了另一个进程的时候,新进程将自动成为父进程所属于的作业的一部分.但可以通过以下方式改变这种行为:
1.打开JOBOBJECT_BASIC_LIMIT_INFORMATION的LimitFlags成员的JOB_OBJECT_LIMIT_BREAKAWAY_OK标志,告诉系统新生成的进程可以在作业外部执行.为此,必须在调用CreateProcess函数时指定新的CREATE_BREAKAWAY_FROM_JOB标志.如果这样做了,但作业并没有打开JOB_OBJECT_LIMIT_BREAKAWAY_OK限制标志,CreateProcess调用就会失败.如果希望由新生成的进程来控制作业,这就是非常有用的一个机制.
2.打开JOBOBJECT_BASIC_LIMIT_INFORMATION的LimitFlags成员的JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK标志.此标志也告诉系统新生成的子进程不是作业的一部分.但是,现在就没有必要向CreateProcess函数传递任何额外的标志.事实上,此标志会强制新进程脱离当前作业.如果进程在设计之初对作业对象一无所知,这个标志就相当有用.
在调用了AssignProcessToJobObject之后,新进程就成为受限制的作业的一部分.然后,调用ResumeThread,使进程的线程可以在作业的限制下执行代码.
Note 13:
Visual Studio没有一个简单的办法来停止正在进行的一次生成,因为它必须知道哪些进程是从它生成的第一个进程生成的.(这非常难.在Microsoft Systems Journal 1998年6月期的Win 32 Q&A专栏讨论
过Developer Studio是如何做到这一点的,可以通过以下网址找到这篇文章:http://www.microsoft.com/msj/0698/win320698.aspx.)也许Visual Studio未来的版本会转而使用作业,因为这样一来,代码的编写会变得更容易,而且可以用作业来做更多的事情.
Note 14:
TerminateJobObject函数可以杀死作业内部的所有进程:
BOOL TerminateJobObject(
HANDLE hJob,
UINT uExitCode);
这类似于为作业内的每一个进程调用TerminateProcess,将所有退出代码设为uExitCode.
Note 15:
GetProcessIoCounters函数可以获得没有放入作业的那些进程的信息,如下所示:
BOOL GetProcessIoCounters(
HANDLE hProcess,
PIO_COUNTERS pIoCounters);