作业

本文介绍了Windows的作业对象,它允许将进程置于一个容器中以限制其行为,创建安全的沙箱环境。作业对象一旦关联到进程,就不能移除,确保了限制的不可规避性。通过函数IsProcessInJob可检查进程是否在作业中。创建作业对象后,可以使用SetInformationJobObject设置限制,如资源使用限制、UI限制和安全限制。AssignProcessToJobObject将进程加入作业,而TerminateJobObject可以结束作业中的所有进程。此外,还能通过QueryInformationJobObject查询作业的统计信息和限制详情。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

windows提供了一个作业内核对象,它允许我们将进程组合在一起并创建一个“沙箱”来限制进程能够做什么,最好将作业对象想象成一个进程容器。但是,创建只包含一个进程的作业同样非常有用,因为这样可以对进程施加平时不能施加的限制。

如果一个进程已与一个作业关联,就无法将当前进程或者它的任何子进程从作业中去除。这个安全特性可以确保进程无法摆脱对它施加的限制。可以利用函数IsProcessInJob验证当前进程是否在一个现有的作业控制之下运行:

BOOL IsProcessInJob(

    HANDLE hProcess,

    HANDLE hJob,

    PBOOL  pbInJob

);

然后,我们可以通过以下调用来创建一个新的作业内核对象:

HANDLE CreateJobObject(

    PSECURITY_ATTRIBUTES psa,

    PCTSTR pszName

);

第一个参数将安全信息与新的作业对象关联,然后告诉系统,是否希望返回的句柄可被继承。最后一个参数对此作业对象进行命名,使能够由另一个进程通过OpenJobObject函数进行访问,如下:

HANDLE CreateJobObject(

    DWORD dwDesiredAccess,

    BOOL bInheritHandle,

    PCTSTR pszName

);

和往常一样,如果确定在自己的代码中不再访问作业对象,就必须调用CloseHandle来关闭它的句柄。务必记住,关闭一个作业对象,不会迫使作业中的所有进程都终止运行。作业对象实际只是加了一个删除标记,只有在作业中的所有进程都已终止运行之后,才会自动销毁。

注意,关闭作业的句柄会导致所有进程都不可访问此作业,即使这个作业仍然存在。如以下代码:

HANDLE hJob = CreateJobObject(NULL,TEXT("Jeff"));

AssignProcessToJobObject(hJob, GetCurrentProcess());

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.

对作业中的进程施加限制

创建好一个作业后,接着一般会根据作业中的进程能够执行哪些操作来建立一个沙箱(即施加限制)。可以向作业应用以下几种类型的限制:

  • 基本限额和扩展基本限额,用于防止作业中的线程独占系统资源。
  • 基本的UI限制,用于防止作业内的进程更改用户界面。
  • 安全限额,用于防止作业内的进程访问安全资源(文件、注册表子项等)。

可以通过以下函数向作业施加限制:

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


JOBOBJECT_BASIC_LIMIT_INFORMATION结构如下所示:

作业

作业
作业
作业

 

 

JOBOBJECT_EXTENDED_LIMIT_INFORMATION结构如下:

作业

 

可以看出,该结构是基本限额的一个超集,其中的PeakProcessMemoryUsed和PeakJobMemoryUsed成员是只读的,分别告诉我们已调拨给作业中的任何一个进程所需的存储空间的峰值,以及已调拨给作业中全部进程所需的存储空间的峰值。其余两个成员ProcessMemoryLimit和JobMemoryLimit分别限制着作业中的任何一个进程或全部进程所使用的已调拨的存储空间。为了设置这样的限制,需要在LimitFlags成员中分别指定JOB_OBJECT_LIMIT_JOB_MEMORY和JOB_OBJECT_LIMIT_PROCESS_MEMORY标志。

再来看看JOBOBJECT_BASIC_UI_RESTRICTIONS结构如下所示:

作业

该结构只有一个数据成员,即UIRestrictionsClass,它容纳着如下表的标志位集合。

作业

作业

最后一个标志JOB_OBJECT_UILIMIT_HANDLES特别有意思。该限制意味着作业中的任何一个进程都不能访问作业外部的进程所创建的用户对象。

所以,如果试图在一个作业内运行Spy++,就只能看到Spy++自己创建的窗口,看不到其他任何窗口。

要为作业中的进程创建一个真正安全的沙箱,对UI句柄进行限制是十分强大的一个能力,不过有时仍然需要让作业内部的一个进程同作业外部的

一个进程通信。

为了做到这一点,一个简单的方法是使用窗口消息。但是如果作业中的进程不能访问UI句柄,那么作业内部的进程就不能向作业外部的进程创建

的一个窗口发送或发布窗口消息,幸运的是,可以用另一个函数来解决这个问题,如下所示:

BOOL UserHandleGrantAccess(

    HANDLE hUserObj,

    HANDLE hJob,

    BOOL bGrant

);

hUserObj参数指定一个用户对象,我们想允许或拒绝作业内部的进程访问此对象。这几乎总是一个窗口句柄,但也有可能是其他用户对象,最后两个参数

hJob和bGrant指出我们要授权哪个作业访问或拒绝哪个作业访问。注意,如果从hJob所标识的作业内的一个进程内调用这个,函数调用会失败——这样可以

防止作业内部的一个进程自己向自己授予一个对象的访问权。

我们可以向作业施加的最后一种限制与安全性有关。注意,一旦应用,安全限制就不能撤销。JOBOBJECT_SECURITY_LIMIT_INFORMATION结构如下所示:

作业

作业

既然已对作业施加限制,自然会想到查询这些限制。通过调用以下函数,很容易实现这一点:

BOOL QueryInformationJobObject(

    HANDLE hJob;

    JOBOBJECTINFOCLASS JobObjectInformationClass,

    PVOID pvJobObjectInformation,

    DWORD cbJobObjectInformationSize,

    PDWORD pdwReturnSize

);

需要传给此函数的参数有:作业的句柄hJob;第二个参数是一个枚举类型,指出我们希望有哪些限制信息;由此函数初始化的数据结构的地址;包含该数据结构的数据块的大小。最后一个参数是pdwReturnSize,它指向由此函数来填充的一个DWORD,指出缓冲区中已填充了多少个字节。如果不感兴趣,可以为此参数传递一个NULL值。

说明:作业中的进程可以调用QueryInformationJobObject获得所属作业的相关信息(为作业句柄参数传递NULL值)。这个技术很有用,因为它使进程能看到自己被施加了哪些限制。不过,如果为作业句柄参数传递NULL值,SetInformationJobObject函数调用会失败——目的是防止进程删除施加于自己身上的限制。

 

将进程放入作业中

我们可以调用以下函数将进程加入一个作业中:

BOOL AssignProcessToJobObject(

    HANDLE hJob,

    HANDLE hProcess

);

这个函数只允许将尚未分配给任何作业的一个进程分配给一个作业,你可以使用IsProcessInJob函数对此检查。一旦进程已经属于作业的一部分,它就不能移动到另一个作业中,也不能称为所谓的“无业的”。还要注意,当作业中的一个进程生成了另一个进程的时候,新进程将自动称为父进程所属于的作业的一部分。但可以通过以下方式改变这种行为。

  • 打开JOBOBJECT_BASIC_LIMIT_INFORMATION的LimitFlags成员的JOB_OBJECT_LIMIT_BREAKAWAY_OK标志,告诉系统新生成的进程可以在作业外部执行。为此,必须在调用CreateProcess函数时指定新的CREATE_BREAKAWAY_FROM_JOB标志。如果这样做了,但作业并没有打开JOB_OBJECT_LIMIT_BREAKAWAY_OK限额标志,CreateProcess调用就会失败。如果希望由新生成的进程来控制作业,这就是非常有用的一个机制。
  • 打开JOBOBJECT_BASIC_LIMIT_INFORMATION的LimitFlags成员的JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK标志。此标志也告诉系统新生成的子进程不应该是作业的一部分。但是,现在就没有必要向CreateProcess函数传递任何额外的标志。事实上,此标志会强制新进程脱离当前作业。如果进程在设计之初对作业对象一无所知,这个标志就相当有用。

终止作业中的所有线程

要“杀死”作业内部所有进程,只需调用以下代码:

BOOL TerminateJobObject(

    HANDLE hJob,

    UINT uExitCode

);

这类似于为作业内的每一个进程调用TerminateProcess,将所有退出代码设为uExitCode。

查询作业统计信息

前面讨论了如何使用QueryInformationJobObject函数来查询作业当前的限制。此外,我们还可以用它来获得作业的统计信息。例如,要获得基本的统计信息,我们可以调用函数QueryInformationJobObject,向第2个参数传递JobObjectBasicAccountingInformation和一个JOBOBJECT_BASIC_ACCOUNTING_INFORMATION结构的地址:

typedef struct _JOBOBJECT_BASIC_ACCOUNTING_INFORMATION{

    LARGE_INTEGER TotalUserTime;

    LARGE_INTEGER TotalKernelTime;

    LARGE_INTEGER ThisPeriodTotalUserTime;

    LARGE_INTEGER ThisPeriodTotalKernelTime;

    DWORD TotalPageFaultCount;

    DWORD TotalProcesses;

    DWORD ActiveProcesses;

    DWORD TotalTerminatedProcesses;

} JOBOBJECT_BASIC_ACCOUNTING_INFORMATION,

    *PJOBOBJECT_BASIC_ACCOUNTING_INFORMATION;

作业

我们也可以通过调用GetProcessTimes函数获得任何一个进程的CPU占用时间信息,即使该进程不属于任何一个作业。

除了查询统计信息,还可以执行一个调用来同时查询基本统计信息和I/O统计信息。为此,要想第2个参数传递JobObjectBasicAndIoAccountingInformation和一个

JOBOBJECT_BASIC_AND_IO_ACCOUNTING_INFORMATION结构的地址:

typedef struct JOBOBJECT_BASIC_AND_IO_ACCOUNTING_INFORMATION{

    JOBOBJECT_BASIC_ACCOUNTING_INFORMATION BasicInfo;

    IO_COUNTERS IoInfo;

} JOBOBJECT_BASIC_AND_IO_ACCOUNTING_INFORMATION,

    *PJOBOBJECT_BASIC_AND_IO_ACCOUNTING_INFORMATION;

可以看出,这个结构返回了JOBOBJECT_BASIC_ACCOUNTING_INFORMATION和一个IO_COUNTERS结构:

typedef struct _IO_COUNTERS{

    ULONGLONG ReadOperationCount;

    ULONGLONG WriteOperationCount;

    ULONGLONG OtherOperationCount;

    ULONGLONG ReadTransferCount;

    ULONGLONG WriteTransferCount;

    ULONGLONG OtherTransferCount;

} IO_COUNTERS, *PIO_COUNTERS;

这个结构指出已由作业中的进程执行过的读操作、写以及非读/写操作的次数(以及这些操作期间传输的字节总数)。顺便说一句,对于那些不属于任何作业的进程,我们可以使用GetProcessIoCounters函数获得未放入作业的那些进程的信息,如下所示:

GetProcessIoCounters(

    HANDLE hProcess,

    PIO_COUNTERS pIoCounters

);

任何时候可以调用QueryInformationJobObject,获得作业中当前正在运行的所有进程的进程ID集。为此,必须首先估算一下作业中有多少个进程,然后分配一个足够大的内存块来容纳由这些进程ID构成的一个数组,另加一个JOBOBJECT_BASIC_PROCESS_ID_LIST结构的大小:

typedef struct _JOBOBJECT_BASIC_PROCESS_ID_LIST {

    DWORD NumberOfAssignedProcess;

    DWORD NumberOfProcessIdsInList;

    DWORD ProcessIdList[1];

}JOBOBJECT_BASIC_PROCESS_ID_LIST, *PJOBOBJECT_BASIC_PROCESS_ID_LIST;

所以,为了获得作业中当前的进程ID集,必须执行以下类似的代码:

void EnumProcessIdsInJob(HANDLE hjob) {

    #define MAX_PROCESS_IDS   10

    DWORD cb = sizeof(JOBOBJECT_BASIC_PROCESS_ID_LIST) +

                 (MAX_PROCESS_IDS - 1) * sizeof(DWORD);

    PJOBOBJECT_BASIC_PROCESS_ID_LIST pjobpil =

        (PJOBOBJECT_BASIC_PROCESS_ID_LIST)_alloca(cb);

    

    pjobpil->NumberOfAssignedProcess = MAX_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;

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值