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);
Note 16:
任何时候都可以调用QueryInformationJobObject,获得作业中当前正在运行的所有进程的进程ID集.为此,必须首先估算一下作业中有多少个进程,然后,分配一个足够大的内存块来容纳由这些进程ID构成的一个数组,另加一个JOBOBJECT_BASIC_PROCESS_ID_LIST结构的大小: typedef struct _JOBOBJECT_BASIC_PROCESS_ID_LIST { DWORD NumberOfAssignedProcesses; DWORD NumberOfProcessIdsInList; DWORD ProcessIdList[1]; } JOBOBJECT_BASIC_PROCESS_ID_LIST, *PJOBOBJECT_BASIC_PROCESS_ID_LIST; 所以,为了获得作业中当前的进程ID集,必须执行以下类似的代码: void EnumProcessIdsInJob(HANDLE hjob) { // I assume that there will never be more // than 10 processes in this job. #define MAX_PROCESS_IDS 10 // Calculate the number of bytes needed for structure & process IDs. DWORD cb = sizeof(JOBOBJECT_BASIC_PROCESS_ID_LIST) + (MAX_PROCESS_IDS – 1) * sizeof(DWORD); // Allocate the block of memory. PJOBOBJECT_BASIC_PROCESS_ID_LIST pjobpil = (PJOBOBJECT_BASIC_PROCESS_ID_LIST)_alloca(cb); // Tell the function the maximum number of processes // that we allocated space for. pjobpil->NumberOfAssignedProcesses = MAX_PROCESS_IDS; // Request the current set of process IDs. QueryInformationJobObject(hjob, JobObjectBasicProcessIdList, pjobpil, cb, &cb); // Enumerate the process IDs. for (DWORD x = 0; x < pjobpil->NumberOfProcessIdsInList; x++) { // Use pjobpil->ProcessIdList[x]... } // Since _alloca was used to allocate the memory, // we don’t need to free it here. } 这就是使用这些函数所能获得的所有信息. Note 17: 操作系统保存着很多与作业相关的信息.这是通过性能计数器来实现的,可以使用Performance Data Helper函数库(PDH.dll)中的函数来获取这些信息.还可以使用Reliability and Performance Monitor("可靠性和性能监视器",在"管理工具"中打开)来查看作业信息.但是,这样只能看到全局命名的作业对象.不过,利用Sysinternals的Process Explorer( http://www.microsoft.com/technet/sysinternals/ProcessesAndThreads/ProcessExplorer.mspx),可以很好地观察作业.默认情况下 作业限制下的所有进程都用棕色来突出显示. Note 18: 作业中的进程如果尚未用完已分配的CPU时间,作业对象就是nonsignaled(无信号)的.一旦用完所有已分配的CPU时间,Windows就会强行杀死作业中的所有进程,作业对象的状态会变成signaled(有信号).通过调用WaitForSingleObject(或者一个类似的函数),可以轻松捕捉到这个事件.顺便提一句,可以调用SetInformationJobObject并授予作业更多的CPU时间,将作业对象重置为原来的nonsignaled状态. Note 19: Microsoft选择在已分配的CPU时间到期时,才将作业的状态变成signaled.在许多作业中,都会有一个父进程一直在运行,直至其所有子进程全部结束.所以,我们可以只等待父进程的句柄,借此得知整个作业何时结束. Note 20: 如何获得一些"高级"的通知(比如进程创建/终止运行).要获得这些额外的通知,必须在自己的应用程序中建立更多的基础结构.具体来讲,你必须创建一个I/O完成端口(completion port)内核对象,并将自己的作业对象与完成端口关联.然后,必须有一个或者多个线程在完成端口上等待并处理作业通知. Note 21: 一旦创建了I/O完成端口,就可以调用SetInformationJobObject将它与一个作业关联起来,如下所示: JOBOBJECT_ASSOCIATE_COMPLETION_PORT joacp; joacp.CompletionKey = 1; // Any value to uniquely identify this job joacp.CompletionPort = hIOCP; // Handle of completion port that // receives notifications SetInformationJobObject(hJob, JobObjectAssociateCompletionPortInformation, &joacp, sizeof(jaocp)); 执行上述代码后,系统将监视作业,只要有事件发生,就会把它们post到I/O完成端口(顺便提一句,你可以调用QueryInformationJobObject来获取completion key和完成端口句柄,但很少有必要这样做).线程通过调用GetQueuedCompletionStatus来监视完成端口: BOOL GetQueuedCompletionStatus( HANDLE hIOCP, PDWORD pNumBytesTransferred, PULONG_PTR pCompletionKey, POVERLAPPED *pOverlapped, DWORD dwMilliseconds); Note 22: 在默认情况下,作业对象是这样配置的:当作业已分配的CPU时间到期时,它的所有进程都会自动终止,而且不会post JOB_OBJECT_MSG_END_OF_JOB_TIME这个通知.如果你想阻止作业对象杀死进程,只是简单地通知你CPU时间到期,就必须像下面这样执行代码: // Create a JOBOBJECT_END_OF_JOB_TIME_INFORMATION structure // and initialize its only member. JOBOBJECT_END_OF_JOB_TIME_INFORMATION joeojti; joeojti.EndOfJobTimeAction = JOB_OBJECT_POST_AT_END_OF_JOB; // Tell the job object what we want it to do when the job time is // exceeded. SetInformationJobObject(hJob, JobObjectEndOfJobTimeInformation, &joeojti, sizeof(joeojti)); 针对EndOfJobTimeAction,你惟一能指定的另一个值就是JOB_OBJECT_TERMINATE_AT_END_OF_JOB.这是创建作业时的默认值. Note 23: 使用psapi.h中的函数(比如GetModuleFileNameEx和GetProcessImageFileName),可以根据进程ID来获得进程的完整路径名称.但是,当作业收到通知,知道一个新进程在它的限制下创建时,前一个函数调用会失败.因为在这个时候,地址空间尚未完全初始化:模块尚未与它建立映射.GetProcessImageFileName则很有意思,因为即使在那种极端情况下,它也能获取完整路径名.但是,路径名的格式近似于内核模式(而不是用户模式)中看到的格式.例如,是\Device\HarddiskVolume1\Windows\System32\notepad.exe,而不是C:\Windows\System32\notepad.exe.这就是你应该依赖于新的QueryFullProcessImageName函数的 原因.该函数在任何情况下都会返回你预期的完整路径名. |